Compare commits
441 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7eee3a13a6 | |||
| 263cf8e2e4 | |||
| ccbf0f8dc2 | |||
| 63027c2dcf | |||
| d46c874604 | |||
| fd4f707b6c | |||
| 1168876dcc | |||
| bdf5afe436 | |||
| 85ff907f05 | |||
| 7ae9db030b | |||
| edf8a12865 | |||
| d081eedd47 | |||
| 8532db5a25 | |||
| 26e783ee2e | |||
| 4bd237abef | |||
| 0ab14de0e2 | |||
| a25b6a20d5 | |||
| e85c14ea45 | |||
| a564178ca2 | |||
| b4a4011b18 | |||
| 3f3d252104 | |||
| 287be2a31c | |||
| 3ef0e4c935 | |||
| ce92e8e12c | |||
| 03597d1b5e | |||
| 36ad9783e5 | |||
| 565db30125 | |||
| 017cbf01f8 | |||
| b16cf762ac | |||
| 0057ac3a5c | |||
| 0edf0561d8 | |||
| 1129d787b5 | |||
| 8a24496801 | |||
| 6cb918c13c | |||
| e3eefadb75 | |||
| eab562b36d | |||
| 20a7c0eead | |||
| f70c5adfcf | |||
| 52246e2e16 | |||
| 4897a5b1a9 | |||
| 2381f68912 | |||
| ad969e8604 | |||
| 31678f9a82 | |||
| 1785af4d9d | |||
| 69d7efe6f0 | |||
| 202c5d0f46 | |||
| 476e907205 | |||
| 724194abe2 | |||
| d57b573683 | |||
| 5d8afe8db7 | |||
| b14f1ff2f9 | |||
| a1301abdb2 | |||
| f897183920 | |||
| c1885ce76a | |||
| e676d53e97 | |||
| f05156b257 | |||
| c8f9d81b3e | |||
| 3a1639ab27 | |||
| 1da1e7bf9f | |||
| 304fbcbaae | |||
| 80f8500f6e | |||
| 78cbcbde9d | |||
|
|
8db1cb179c | ||
| a065e47545 | |||
| 5271029b84 | |||
| 7c4fb53baa | |||
| 1c2fb471a6 | |||
| 85f72b14e0 | |||
| 99261beb08 | |||
| d34be4f18d | |||
| 3d26832e54 | |||
| cd80c9fc0e | |||
| 672ca07782 | |||
| b06313c4eb | |||
| 31be9b0ce8 | |||
| d69a53b4c8 | |||
| 7aa5d50a6c | |||
| d047c632ed | |||
| 9eb5c04a33 | |||
| 498432d968 | |||
| e04eb57202 | |||
| 25fe8350a7 | |||
| 9a10386e65 | |||
| 75ab8646e8 | |||
| 37ef42e53d | |||
| b2aa1b75f3 | |||
| 31bcd3481c | |||
| 966078c339 | |||
| fdcd816235 | |||
| 054d8ab7ef | |||
| 0327359300 | |||
| c61a4b0c04 | |||
| 950f95cef1 | |||
| d40670fef6 | |||
| d6e5e7941f | |||
| a06e9027c6 | |||
| 8fb0595ef8 | |||
| d3808a887e | |||
| 1ed379c23d | |||
| 0e66db1d8a | |||
| bac16d22a9 | |||
| 8b4de82142 | |||
| 35930c52a4 | |||
| 293e71e619 | |||
| 7150189a0d | |||
| 0e951d7089 | |||
| ef89c3001b | |||
| b538c7b5de | |||
| 6c234e7fa4 | |||
| 407d8f2da2 | |||
| 9b755cd0be | |||
| 19f9d4bda9 | |||
| de744337ad | |||
| 63ba6d2da1 | |||
| 79b77ee6c3 | |||
| 0e2935f844 | |||
| 0ad6a23daa | |||
| 35398ebf38 | |||
| 62b028cf56 | |||
| ad676915bb | |||
| 61ebb22a85 | |||
| 8348b93b30 | |||
| 550e11b488 | |||
| ed10ea496d | |||
| 6e5c7510d0 | |||
| 68d0393915 | |||
| f1e07f0a3b | |||
| 90100f6f5b | |||
| 995966e952 | |||
| a90eb8248f | |||
| ffca28c67d | |||
| 20a2a95ce6 | |||
| 145fd02ba8 | |||
| 196a5a8cfa | |||
| 439e9b29d1 | |||
| a3f1a0e96d | |||
| c81e349e90 | |||
| 30068a5b11 | |||
| 036b6b23a8 | |||
| 128eb44003 | |||
| 0f72b2048e | |||
| 1e4045ab7a | |||
| b871549087 | |||
| 58760831dc | |||
| 0a8ab00637 | |||
| 8396801d80 | |||
| 19af9894e5 | |||
| 7293f5a8fa | |||
| 9d12609147 | |||
| 2c7089504f | |||
| 8fbff23bee | |||
| 0c5078e3f3 | |||
| d143a18838 | |||
| ba889e442c | |||
| 4511ecbf0a | |||
| fe37bebd2c | |||
| d987436892 | |||
| 9f4e19104f | |||
| c0568075e1 | |||
| 13415fb065 | |||
| ca13d1e364 | |||
| bf916dd8d5 | |||
| 4aa168ebcc | |||
| 2cea01f367 | |||
| 2ec0aea19c | |||
| 846584d6fe | |||
| 1b5912a1bf | |||
| 954dbfa425 | |||
| 42ec90a5df | |||
| 0b4350c8ae | |||
| da28805882 | |||
| 6551e24069 | |||
| 28ef8cb33a | |||
| d4b0dee037 | |||
| 799d3e1ddd | |||
| e6f3bac946 | |||
| 05c7e71794 | |||
| 41b980d118 | |||
| 3efd99cdc8 | |||
| 119eb7965b | |||
| f4421f0334 | |||
| 688f2f4b30 | |||
| 87300bf48a | |||
| 8cbad47acc | |||
| 241615238c | |||
| 319e6cdd77 | |||
| d54e93ad85 | |||
| efe0002e02 | |||
| e58d92c399 | |||
| fa18138c3b | |||
| 2a5d6e5157 | |||
| c32549fa87 | |||
| 2689e39d70 | |||
| 25f2310c25 | |||
| 0bed5ecf41 | |||
| ec2f146095 | |||
| 75a84baa42 | |||
| 1bc7ebc92a | |||
| d620f577c1 | |||
| 7af1b40222 | |||
| 82f1539815 | |||
| e38ba012a8 | |||
| a0322718c1 | |||
| 01ca12f80e | |||
| 3376b4fa7c | |||
| 308b65e2f6 | |||
| fa5b73b5fe | |||
| 0fd088320d | |||
| a109c631cd | |||
| cf61a74fa8 | |||
| 8fa4effb6b | |||
| 5ca69bbe24 | |||
| 8816468ba0 | |||
| e9f17c585c | |||
| 373ca377a2 | |||
| 770420a5b2 | |||
| 22b1b6bc73 | |||
| 18fd4bb9d2 | |||
| ca333a93e3 | |||
| 4c404225d2 | |||
| 9f86a4e0f9 | |||
| 6273263c4e | |||
| 0b6b8df1a9 | |||
| fe730a19df | |||
| 636ef87cb1 | |||
| d333901576 | |||
| d104bcc1fa | |||
| dbfbc2c347 | |||
| dd620c14d3 | |||
| 744098abc7 | |||
| d41554271d | |||
| 23bfbf9f4a | |||
| f52ed8c791 | |||
| 604371a2e1 | |||
| 44cec2d2fb | |||
| cbe9ef7303 | |||
| 002970484d | |||
| f063ceb4e5 | |||
| 1eaefffe04 | |||
| 1291d21216 | |||
| 1c198f0133 | |||
| 23bf656f82 | |||
| 30fae67f6c | |||
| 2726955034 | |||
| e433bc3319 | |||
| 71df812170 | |||
| abb8540c12 | |||
| 12e0c23ea9 | |||
| b98ecdd0a4 | |||
| 7314c67357 | |||
| 6b74e01f0a | |||
| 6becc08e18 | |||
| f66a990103 | |||
| c3b9772512 | |||
| 6bb1cc683c | |||
| b5473e05e7 | |||
| 939ca47e3c | |||
| c16e5f1e47 | |||
| 059f917acc | |||
| 43b08f22dd | |||
| aa7e4a3d3e | |||
| 18f2b44941 | |||
| b2925285a2 | |||
| c0a6a3c082 | |||
| 7c7edcf959 | |||
| dee5053345 | |||
| 58e071e77b | |||
| a7e85acd67 | |||
| 176d184d91 | |||
| 32543d04a0 | |||
| 4b43ee86c0 | |||
| 25959be98f | |||
| cb6680304f | |||
| 40646f16bc | |||
| 55e7ee2428 | |||
| a2930577d3 | |||
| e572f50de7 | |||
| 47b7f5d85a | |||
| 8ac29e4378 | |||
| be81a31302 | |||
| 2e44a1ef31 | |||
| d182cc1418 | |||
| d3004019c6 | |||
| fca49c6302 | |||
| 8b085009a9 | |||
| 2ba6d190f0 | |||
| 8c5a94a12a | |||
| bf9e1f0319 | |||
| 903baf7f38 | |||
| 5b585ebba7 | |||
| 34bae5dc7b | |||
| 4f97859533 | |||
| 4c55e65863 | |||
| 016f0be480 | |||
| fbf35d6d77 | |||
| 30ee1c717e | |||
| e3286571b1 | |||
| ece39790d2 | |||
| eedadb0a3f | |||
| a01dd5dace | |||
| b6f0fa9776 | |||
| cb03f8e23c | |||
| fbff094227 | |||
| e08cf455ee | |||
| de57b4c8a8 | |||
| 9fef1da261 | |||
| 1f6a1a49a6 | |||
| fddd294a03 | |||
| 00ffe6973e | |||
| f73096e439 | |||
| e26ab02862 | |||
| c2b9c1267b | |||
| a400a85dc9 | |||
| f5a4b49935 | |||
| 2dabe7959f | |||
| dcfc406481 | |||
| f680d36d2b | |||
| d44aa04e44 | |||
| 4594dff4a6 | |||
| be1093d51f | |||
| 546aa27bb1 | |||
| dfb2f687d0 | |||
| 9e344aed64 | |||
| ba77555cab | |||
| e8e15444c9 | |||
| cdd859ba50 | |||
| ab85b3712a | |||
| fc3732fd0c | |||
| 4652c60e64 | |||
| 00f7cba8fc | |||
| e425fa9209 | |||
| f1e2f60836 | |||
| db7edaf802 | |||
| 58a7ecd723 | |||
| bff1a4acf9 | |||
| b287fb41c4 | |||
| 5e0f81ce53 | |||
| bd8b3fa00f | |||
| 899fb888c3 | |||
| 2d3820952f | |||
| f6b043fa39 | |||
| 8bce840497 | |||
| aff3e83bab | |||
| 85a28bc7ca | |||
| 452aa2e3a0 | |||
| 784cee6f04 | |||
| ca30998a5a | |||
| 476adcdc2e | |||
| 2c6142e6c4 | |||
| cae1eef659 | |||
| 2dfb60e63d | |||
| 8524a1116a | |||
| 60ccbc72bb | |||
| 8d3813f3ee | |||
| aa46000abf | |||
| 6059c16a6e | |||
| 2e805abef7 | |||
| 06f143915c | |||
| be20aa77b9 | |||
| 7b2c67d130 | |||
| 9480c42181 | |||
| 17c163c71c | |||
| 3ee90fb767 | |||
| 7f9476236d | |||
| 2d2cf621bd | |||
| bb1acd5003 | |||
| c00ddd410a | |||
| 35c51143bc | |||
| 16cd2f82be | |||
| 34e027525f | |||
| 331823e34f | |||
| 18daa170c5 | |||
| 7e8978044f | |||
| 2811396234 | |||
| 212ce2c71c | |||
| f76ef718ed | |||
| bd40d43686 | |||
| 2954a93b4d | |||
| 93d1d55cfa | |||
| 1c7951a530 | |||
| 84a53ea065 | |||
| 9dcf526b4c | |||
| 032cfc5bff | |||
| 5c9450d06d | |||
| f811dd9a6c | |||
| d13da3141d | |||
| d4bab870a4 | |||
| 6ef7e74402 | |||
| 55ce001705 | |||
| 77c3fc291d | |||
| ff849092ff | |||
| d0cb074912 | |||
| fdedde829e | |||
| 4364d87bf7 | |||
| 2232f1a92d | |||
| 251e11a7e8 | |||
| d4e4290dc5 | |||
| 9fd57c5d7b | |||
| 873ce0a30c | |||
| 36297ca09d | |||
| 0ec3421a79 | |||
| 21d99ae643 | |||
| 4dc3335a86 | |||
| c9627779ba | |||
| 3f1fae8906 | |||
| 2ae12fade0 | |||
| c66c961297 | |||
| 8da5773dcd | |||
| 18106db367 | |||
| 5eb6f3cbfd | |||
| 614b07c01a | |||
| 2c84c326dd | |||
| 0ceb59c778 | |||
| 137ce0a3a7 | |||
| 59d2b999c2 | |||
| d80ffbaff5 | |||
| 8c45e90ce1 | |||
| ed7c1a4685 | |||
| e32966bee2 | |||
| 5e779cc5f9 | |||
| 5e78f623f5 | |||
| 8e90a71dfc | |||
| 9ebc95885e | |||
| e1136703a5 | |||
| c446d1dc85 | |||
| de49956aef | |||
| efde43fadb | |||
| af2a714b76 | |||
| bfaa73b660 | |||
| a7c818b88b | |||
| e1fb450544 | |||
| acb2910b70 | |||
| 0dd1a6fd69 | |||
| 5f3374a073 | |||
| 3049a0b554 | |||
| fa7a411e34 | |||
| 6bf1c491a0 | |||
| 8e1f2c147e | |||
| c4f4d24064 | |||
| 30578bdbb0 | |||
| 0e771a7db3 |
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal 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
2
.gitignore
vendored
@@ -1 +1 @@
|
|||||||
Meta
|
scratch.lua
|
||||||
|
|||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "Meta"]
|
||||||
|
path = Meta
|
||||||
|
url = https://git.site.quack-lab.dev/dave/wow_Meta
|
||||||
5
.luacheckrc
Symbolic link
5
.luacheckrc
Symbolic link
@@ -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
14
.luarc.json
Symbolic link
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"workspace": {
|
||||||
|
"library": [
|
||||||
|
"./Meta"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"diagnostics.disable": [
|
||||||
|
"unused-local",
|
||||||
|
"unused-vararg"
|
||||||
|
],
|
||||||
|
"diagnostics.globals": [
|
||||||
|
"aura_env"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
959
Heimdall.lua
959
Heimdall.lua
@@ -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...
|
local function init()
|
||||||
-- But that would not be trivial as of now, I can't think of a way to do it sensibly
|
---@class Heimdall_Data
|
||||||
-- TODO: Implement auto grouping via agent, maybe find "+" or something
|
---@field config HeimdallConfig
|
||||||
|
if not Heimdall_Data then Heimdall_Data = {} end
|
||||||
local function init()
|
|
||||||
---@class Heimdall_Data
|
---@class InitTable
|
||||||
---@field who { data: table<string, Player> }
|
---@field Init fun(): nil
|
||||||
---@field stinkies table<string, boolean>
|
|
||||||
if not Heimdall_Data then Heimdall_Data = {} end
|
---@class HeimdallShared
|
||||||
if not Heimdall_Data.config then Heimdall_Data.config = {} end
|
---@field raceMap table<string, string>
|
||||||
|
---@field classColors table<string, string>
|
||||||
-- We don't care about these persisting
|
---@field messenger HeimdallMessengerData
|
||||||
-- Actually we don't want some of them to persist
|
---@field who HeimdallWhoData
|
||||||
-- For those we DO we use (global) Heimdall_Data
|
---@field stinkyTracker StinkyTrackerData
|
||||||
|
---@field agentTracker AgentTrackerData
|
||||||
---@class HeimdallData
|
---@field networkNodes string[]
|
||||||
---@field config HeimdallConfig
|
---@field network HeimdallNetworkData
|
||||||
---@field raceMap table<string, string>
|
---@field networkMessenger HeimdallNetworkMessengerData
|
||||||
---@field classColors table<string, string>
|
---@field stinkyCache HeimdallStinkyCacheData
|
||||||
---@field messenger HeimdallMessengerData
|
---@field _L fun(key: string, locale: string): string
|
||||||
---@field who HeimdallWhoData
|
---@field _Locale Localization
|
||||||
---@field dumpTable fun(table: any, depth?: number): nil
|
---@field VERSION string
|
||||||
---@field utf8len fun(input: string): number
|
---@field dump fun(table: any, msg?: string, depth?: number): nil
|
||||||
---@field padString fun(input: string, targetLength: number, left?: boolean): string
|
---@field utf8len fun(input: string): number
|
||||||
---@field GetOrDefault fun(table: table<any, any>, keys: string[], default: any): any
|
---@field padString fun(input: string, targetLength: number, left?: boolean): string
|
||||||
---@field Whoer { Init: fun() }
|
---@field GetOrDefault fun(table: table<any, any>, keys: string[], default: any): any
|
||||||
---@field Messenger { Init: fun() }
|
---@field Split fun(input: string, deliminer: string): string[]
|
||||||
---@field Spotter { Init: fun() }
|
---@field IsStinky fun(name: string): boolean
|
||||||
---@field DeathReporter { Init: fun() }
|
---@field Memoize fun(f: function): function
|
||||||
|
---@field GetLocaleForChannel fun(channel: string): string
|
||||||
--- Config ---
|
---@field WhoQueryService WhoQueryService
|
||||||
---@class HeimdallConfig
|
---@field AchievementSniffer AchievementSniffer
|
||||||
---@field spotter HeimdallSpotterConfig
|
---@field AgentTracker AgentTracker
|
||||||
---@field who HeimdallWhoConfig
|
---@field BonkDetector BonkDetector
|
||||||
---@field messenger HeimdallMessengerConfig
|
---@field Bully Bully
|
||||||
---@field deathReporter HeimdallDeathReporterConfig
|
---@field CombatAlerter CombatAlerter
|
||||||
---@field whisperNotify table<string, string>
|
---@field Commander Commander
|
||||||
---@field stinkies table<string, boolean>
|
---@field Config Config
|
||||||
|
---@field Configurator Configurator
|
||||||
---@class HeimdallSpotterConfig
|
---@field DeathReporter DeathReporter
|
||||||
---@field enabled boolean
|
---@field Dueler Dueler
|
||||||
---@field everyone boolean
|
---@field Echoer Echoer
|
||||||
---@field hostile boolean
|
---@field Emoter Emoter
|
||||||
---@field alliance boolean
|
---@field Inviter Inviter
|
||||||
---@field stinky boolean
|
---@field Macroer Macroer
|
||||||
---@field notifyChannel string
|
---@field Messenger Messenger
|
||||||
---@field zoneOverride string?
|
---@field MinimapTagger MinimapTagger
|
||||||
---@field throttleTime number
|
---@field Network Network
|
||||||
|
---@field NetworkMessenger NetworkMessenger
|
||||||
---@class HeimdallWhoConfig
|
---@field Noter Noter
|
||||||
---@field enabled boolean
|
---@field Sniffer Sniffer
|
||||||
---@field ignored table<string, boolean>
|
---@field Spotter Spotter
|
||||||
---@field notifyChannel string
|
---@field StinkyCache StinkyCache
|
||||||
---@field ttl number
|
---@field StinkyTracker StinkyTracker
|
||||||
---@field doWhisper boolean
|
---@field Whoer Whoer
|
||||||
---@field zoneNotifyFor table<string, boolean>
|
---@field ChatSniffer ChatSniffer
|
||||||
|
|
||||||
---@class HeimdallMessengerConfig
|
--- Config ---
|
||||||
---@field enabled boolean
|
---@class HeimdallConfig
|
||||||
|
---@field spotter HeimdallSpotterConfig
|
||||||
---@class HeimdallDeathReporterConfig
|
---@field who HeimdallWhoConfig
|
||||||
---@field enabled boolean
|
---@field messenger HeimdallMessengerConfig
|
||||||
---@field throttle number
|
---@field deathReporter HeimdallDeathReporterConfig
|
||||||
---@field doWhisper boolean
|
---@field inviter HeimdallInviterConfig
|
||||||
---@field notifyChannel string
|
---@field dueler HeimdallDuelerConfig
|
||||||
---@field zoneOverride string?
|
---@field agentTracker HeimdallAgentTrackerConfig
|
||||||
---@field duelThrottle number
|
---@field emoter HeimdallEmoterConfig
|
||||||
|
---@field echoer HeimdallEchoerConfig
|
||||||
--- Data ---
|
---@field macroer HeimdallMacroerConfig
|
||||||
---@class HeimdallMessengerData
|
---@field commander HeimdallCommanderConfig
|
||||||
---@field queue table<string, Message>
|
---@field stinkyTracker HeimdallStinkyTrackerConfig
|
||||||
---@field ticker number?
|
---@field combatAlerter HeimdallCombatAlerterConfig
|
||||||
|
---@field sniffer HeimdallSnifferConfig
|
||||||
---@class HeimdallWhoData
|
---@field noter HeimdallNoterConfig
|
||||||
---@field updateTicker number?
|
---@field network HeimdallNetworkConfig
|
||||||
---@field whoTicker number?
|
---@field networkMessenger HeimdallNetworkMessengerConfig
|
||||||
---@field ignored table<string, boolean>
|
---@field configurator HeimdallConfiguratorConfig
|
||||||
|
---@field stinkyCache HeimdallStinkyCacheConfig
|
||||||
data.GetOrDefault = function(table, keys, default)
|
---@field achievementSniffer HeimdallAchievementSnifferConfig
|
||||||
local value = default
|
---@field chatSniffer HeimdallChatSnifferConfig
|
||||||
if not table then return value end
|
---@field whisperNotify table<string, string>
|
||||||
if not keys then return value end
|
---@field addonPrefix string
|
||||||
|
---@field stinkies table<string, boolean>
|
||||||
local traverse = table
|
---@field agents table<string, string>
|
||||||
for i = 1, #keys do
|
---@field scale number
|
||||||
local key = keys[i]
|
---@field notes table<string, Note[]>
|
||||||
if traverse[key] ~= nil then
|
---@field channelLocale table<string, string>
|
||||||
traverse = traverse[key]
|
---@field locale string
|
||||||
else
|
---@field debug boolean
|
||||||
break
|
|
||||||
end
|
shared.GetOrDefault = function(table, keys, default)
|
||||||
|
local value = default
|
||||||
if i == #keys then
|
if not table then return value end
|
||||||
value = traverse
|
if not keys then return value end
|
||||||
end
|
|
||||||
end
|
local traverse = table
|
||||||
return value
|
for i = 1, #keys do
|
||||||
end
|
local key = keys[i]
|
||||||
|
if traverse[key] ~= nil then
|
||||||
data.messenger = {
|
traverse = traverse[key]
|
||||||
queue = {}
|
else
|
||||||
}
|
break
|
||||||
data.who = {
|
end
|
||||||
ignored = {},
|
|
||||||
}
|
if i == #keys then value = traverse end
|
||||||
--/run Heimdall_Data.config = {who={enabled=true},deathReporter={enabled=true}}
|
end
|
||||||
--/run Heimdall_Data.config = {deathReporter={enabled=true}}
|
return value
|
||||||
--/run Heimdall_Data.config = {deathReporter={enabled=false},spotter={enabled=false}}
|
end
|
||||||
--/run Heimdall_Data.config = {deathReporter={enabled=false},spotter={enabled=true,everyone=true}}
|
|
||||||
data.config = {
|
--/run Heimdall_Data.config.who.queries="g-\"БеспредеЛ\"|ally"
|
||||||
spotter = {
|
Heimdall_Data.config = {
|
||||||
enabled = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "enabled" }, true),
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "debug" }, false),
|
||||||
everyone = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "everyone" }, false),
|
chatSniffer = {
|
||||||
hostile = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "hostile" }, true),
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "chatSniffer", "enabled" }, false),
|
||||||
alliance = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "alliance" }, true),
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "chatSniffer", "debug" }, false),
|
||||||
stinky = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "stinky" }, true),
|
},
|
||||||
notifyChannel = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "notifyChannel" }, "Agent"),
|
spotter = {
|
||||||
zoneOverride = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "zoneOverride" }, nil),
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "enabled" }, true),
|
||||||
throttleTime = data.GetOrDefault(Heimdall_Data, { "config", "spotter", "throttleTime" }, 10)
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "debug" }, false),
|
||||||
},
|
everyone = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "everyone" }, false),
|
||||||
who = {
|
hostile = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "hostile" }, true),
|
||||||
enabled = data.GetOrDefault(Heimdall_Data, { "config", "who", "enabled" }, false),
|
alliance = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "alliance" }, true),
|
||||||
ignored = data.GetOrDefault(Heimdall_Data, { "config", "who", "ignored" }, {}),
|
stinky = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "stinky" }, true),
|
||||||
notifyChannel = data.GetOrDefault(Heimdall_Data, { "config", "who", "notifyChannel" }, "Agent"),
|
channels = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "channels" }, { "Agent" }),
|
||||||
ttl = data.GetOrDefault(Heimdall_Data, { "config", "who", "ttl" }, 10),
|
zoneOverride = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "zoneOverride" }, nil),
|
||||||
doWhisper = data.GetOrDefault(Heimdall_Data, { "config", "who", "doWhisper" }, true),
|
throttleTime = shared.GetOrDefault(Heimdall_Data, { "config", "spotter", "throttleTime" }, 10),
|
||||||
zoneNotifyFor = data.GetOrDefault(Heimdall_Data, { "config", "who", "zoneNotifyFor" }, {
|
},
|
||||||
["Orgrimmar"] = true,
|
who = {
|
||||||
["Thunder Bluff"] = true,
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "who", "enabled" }, false),
|
||||||
["Undercity"] = true,
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "who", "debug" }, false),
|
||||||
["Durotar"] = true,
|
ignored = shared.GetOrDefault(Heimdall_Data, { "config", "who", "ignored" }, {}),
|
||||||
["Echo Isles"] = true,
|
channels = shared.GetOrDefault(Heimdall_Data, { "config", "who", "channels" }, { "Agent" }),
|
||||||
["Valley of Trials"] = true,
|
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" }, {
|
||||||
messenger = {
|
["Orgrimmar"] = true,
|
||||||
enabled = data.GetOrDefault(Heimdall_Data, { "config", "messenger", "enabled" }, true),
|
["Thunder Bluff"] = true,
|
||||||
},
|
["Undercity"] = true,
|
||||||
deathReporter = {
|
["Durotar"] = true,
|
||||||
enabled = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "enabled" }, false),
|
["Echo Isles"] = true,
|
||||||
throttle = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "throttle" }, 10),
|
["Valley of Trials"] = true,
|
||||||
doWhisper = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "doWhisper" }, true),
|
}),
|
||||||
notifyChannel = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "notifyChannel" }, "Agent"),
|
queries = shared.GetOrDefault(Heimdall_Data, { "config", "who", "queries" }, ""),
|
||||||
zoneOverride = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "zoneOverride" }, nil),
|
},
|
||||||
duelThrottle = data.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "duelThrottle" }, 5),
|
messenger = {
|
||||||
},
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "messenger", "enabled" }, true),
|
||||||
whisperNotify = data.GetOrDefault(Heimdall_Data, { "config", "whisperNotify" }, {
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "messenger", "debug" }, false),
|
||||||
"Extazyk",
|
interval = shared.GetOrDefault(Heimdall_Data, { "config", "messenger", "interval" }, 0.2),
|
||||||
"Smokefire",
|
},
|
||||||
"Smokemantra",
|
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),
|
||||||
"Frosstmorn",
|
doWhisper = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "doWhisper" }, true),
|
||||||
"Pulsjkee",
|
channels = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "channels" }, { "Agent" }),
|
||||||
"Paskoo",
|
zoneOverride = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "zoneOverride" }, nil),
|
||||||
"Totleta",
|
duelThrottle = shared.GetOrDefault(Heimdall_Data, { "config", "deathReporter", "duelThrottle" }, 5),
|
||||||
"Healleta",
|
},
|
||||||
"Deathleta",
|
inviter = {
|
||||||
"Shootleta",
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "enabled" }, false),
|
||||||
"Stableta"
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "debug" }, false),
|
||||||
}),
|
channels = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "channels" }, { "Agent" }),
|
||||||
stinkies = data.GetOrDefault(Heimdall_Data, { "config", "stinkies" }, {
|
keyword = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "keyword" }, "+"),
|
||||||
"Ahhahahh",
|
allAssist = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "allAssist" }, false),
|
||||||
"Aye",
|
agentsAssist = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "agentsAssist" }, false),
|
||||||
"Bbd",
|
throttle = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "throttle" }, 1),
|
||||||
"Blessly",
|
kickOffline = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "kickOffline" }, false),
|
||||||
"Bunkkeer",
|
cleanupInterval = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "cleanupInterval" }, 10),
|
||||||
"Calmer",
|
afkThreshold = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "afkThreshold" }, 300),
|
||||||
"Chuvirloeban",
|
listeningChannel = shared.GetOrDefault(Heimdall_Data, { "config", "inviter", "listeningChannel" }, {}),
|
||||||
"Clairvoyant",
|
},
|
||||||
"Dewdew",
|
dueler = {
|
||||||
"Dwxrfshaman",
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "dueler", "enabled" }, false),
|
||||||
"Ebanirot",
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "dueler", "debug" }, false),
|
||||||
"Heger",
|
declineOther = shared.GetOrDefault(Heimdall_Data, { "config", "dueler", "declineOther" }, false),
|
||||||
"Hmor",
|
},
|
||||||
"Joule",
|
bully = {
|
||||||
"Kaøs",
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "bully", "enabled" }, false),
|
||||||
"Kromsaevmode",
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "bully", "debug" }, false),
|
||||||
"Kugisara",
|
},
|
||||||
"Lax",
|
agentTracker = {
|
||||||
"Negron",
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "agentTracker", "enabled" }, false),
|
||||||
"Oakskin",
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "agentTracker", "debug" }, false),
|
||||||
"Pizdosorkam",
|
channels = shared.GetOrDefault(Heimdall_Data, { "config", "agentTracker", "channels" }, { "Agent" }),
|
||||||
"Pussymism",
|
},
|
||||||
"Rattenfenger",
|
emoter = {
|
||||||
"Riener",
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "emoter", "enabled" }, false),
|
||||||
"Rollbot",
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "emoter", "debug" }, false),
|
||||||
"Samuraqt",
|
channels = shared.GetOrDefault(Heimdall_Data, { "config", "emoter", "channels" }, { "Agent" }),
|
||||||
"Sekiiro",
|
prefix = shared.GetOrDefault(Heimdall_Data, { "config", "emoter", "prefix" }, ""),
|
||||||
"Shadowmilf",
|
},
|
||||||
"Sonikblaster",
|
echoer = {
|
||||||
"Srakonyh",
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "echoer", "enabled" }, false),
|
||||||
"Stuffo",
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "echoer", "debug" }, false),
|
||||||
"Subaruwrxsti",
|
channels = shared.GetOrDefault(Heimdall_Data, { "config", "echoer", "channels" }, { "Agent" }),
|
||||||
"Sukunexd",
|
prefix = shared.GetOrDefault(Heimdall_Data, { "config", "echoer", "prefix" }, ""),
|
||||||
"Tomoki",
|
},
|
||||||
"Unwashed",
|
macroer = {
|
||||||
"Voitas",
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "macroer", "enabled" }, false),
|
||||||
"Wataru",
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "macroer", "debug" }, false),
|
||||||
"Yooshima",
|
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),
|
||||||
data.raceMap = {
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "messageDelegator", "debug" }, false),
|
||||||
["Orc"] = "Horde",
|
delegates = shared.GetOrDefault(Heimdall_Data, { "config", "messageDelegator", "delegates" }, {}),
|
||||||
["Undead"] = "Horde",
|
masterChannel = shared.GetOrDefault(
|
||||||
["Tauren"] = "Horde",
|
Heimdall_Data,
|
||||||
["Troll"] = "Horde",
|
{ "config", "messageDelegator", "masterChannel" },
|
||||||
["Blood Elf"] = "Horde",
|
"Agent"
|
||||||
["Goblin"] = "Horde",
|
),
|
||||||
["Human"] = "Alliance",
|
},
|
||||||
["Dwarf"] = "Alliance",
|
sniffer = {
|
||||||
["Night Elf"] = "Alliance",
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "sniffer", "enabled" }, false),
|
||||||
["Gnome"] = "Alliance",
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "sniffer", "debug" }, false),
|
||||||
["Draenei"] = "Alliance",
|
channels = shared.GetOrDefault(Heimdall_Data, { "config", "sniffer", "channels" }, { "Agent" }),
|
||||||
["Worgen"] = "Alliance",
|
throttle = shared.GetOrDefault(Heimdall_Data, { "config", "sniffer", "throttle" }, 10),
|
||||||
["Vulpera"] = "Horde",
|
zoneOverride = shared.GetOrDefault(Heimdall_Data, { "config", "sniffer", "zoneOverride" }, nil),
|
||||||
["Nightborne"] = "Horde",
|
stinky = shared.GetOrDefault(Heimdall_Data, { "config", "sniffer", "stinky" }, true),
|
||||||
["Zandalari Troll"] = "Horde",
|
},
|
||||||
["Kul Tiran"] = "Alliance",
|
minimapTagger = {
|
||||||
["Dark Iron Dwarf"] = "Alliance",
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "enabled" }, false),
|
||||||
["Void Elf"] = "Alliance",
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "debug" }, false),
|
||||||
["Lightforged Draenei"] = "Alliance",
|
channels = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "channels" }, { "Agent" }),
|
||||||
["Mechagnome"] = "Alliance",
|
throttle = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "throttle" }, 10),
|
||||||
["Mag'har Orc"] = "Horde"
|
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),
|
||||||
data.classColors = {
|
tagSoundFile = shared.GetOrDefault(
|
||||||
["Warrior"] = "C69B6D",
|
Heimdall_Data,
|
||||||
["Paladin"] = "F48CBA",
|
{ "config", "minimapTagger", "tagSoundFile" },
|
||||||
["Hunter"] = "AAD372",
|
"MGSSpot.ogg"
|
||||||
["Rogue"] = "FFF468",
|
),
|
||||||
["Priest"] = "FFFFFF",
|
tagSoundThrottle = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "tagSoundThrottle" }, 0),
|
||||||
["Death Knight"] = "C41E3A",
|
tagTextureFile = shared.GetOrDefault(
|
||||||
["Shaman"] = "0070DD",
|
Heimdall_Data,
|
||||||
["Mage"] = "3FC7EB",
|
{ "config", "minimapTagger", "tagTextureFile" },
|
||||||
["Warlock"] = "8788EE",
|
"Aura4.tga"
|
||||||
["Monk"] = "00FF98",
|
),
|
||||||
["Druid"] = "FF7C0A",
|
---
|
||||||
["Demon Hunter"] = "A330C9"
|
alertTTL = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "alertTTL" }, 1),
|
||||||
}
|
alertSound = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "alertSound" }, false),
|
||||||
|
alertSoundFile = shared.GetOrDefault(
|
||||||
---@param input string
|
Heimdall_Data,
|
||||||
---@return number
|
{ "config", "minimapTagger", "alertSoundFile" },
|
||||||
data.utf8len = function(input)
|
"OOF.ogg"
|
||||||
if not input then
|
),
|
||||||
return 0
|
alertSoundThrottle = shared.GetOrDefault(
|
||||||
end
|
Heimdall_Data,
|
||||||
local len = 0
|
{ "config", "minimapTagger", "alertSoundThrottle" },
|
||||||
local i = 1
|
0
|
||||||
local n = #input
|
),
|
||||||
while i <= n do
|
alertTextureFile = shared.GetOrDefault(
|
||||||
local c = input:byte(i)
|
Heimdall_Data,
|
||||||
if c >= 0 and c <= 127 then
|
{ "config", "minimapTagger", "alertTextureFile" },
|
||||||
i = i + 1
|
"Aura27.tga"
|
||||||
elseif c >= 194 and c <= 223 then
|
),
|
||||||
i = i + 2
|
---
|
||||||
elseif c >= 224 and c <= 239 then
|
combatTTL = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "combatTTL" }, 1),
|
||||||
i = i + 3
|
combatSound = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "combatSound" }, false),
|
||||||
elseif c >= 240 and c <= 244 then
|
combatSoundFile = shared.GetOrDefault(
|
||||||
i = i + 4
|
Heimdall_Data,
|
||||||
else
|
{ "config", "minimapTagger", "combatSoundFile" },
|
||||||
i = i + 1
|
"StarScream.ogg"
|
||||||
end
|
),
|
||||||
len = len + 1
|
combatSoundThrottle = shared.GetOrDefault(
|
||||||
end
|
Heimdall_Data,
|
||||||
return len
|
{ "config", "minimapTagger", "combatSoundThrottle" },
|
||||||
end
|
2
|
||||||
---@param input string
|
),
|
||||||
---@param targetLength number
|
combatTextureFile = shared.GetOrDefault(
|
||||||
---@param left boolean
|
Heimdall_Data,
|
||||||
---@return string
|
{ "config", "minimapTagger", "combatTextureFile" },
|
||||||
data.padString = function(input, targetLength, left)
|
"Aura58.tga"
|
||||||
left = left or false
|
),
|
||||||
local len = data.utf8len(input)
|
---
|
||||||
if len < targetLength then
|
helpTTL = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "helpTTL" }, 1),
|
||||||
if left then
|
helpSound = shared.GetOrDefault(Heimdall_Data, { "config", "minimapTagger", "helpSound" }, false),
|
||||||
input = input .. string.rep(" ", targetLength - len)
|
helpSoundFile = shared.GetOrDefault(
|
||||||
else
|
Heimdall_Data,
|
||||||
input = string.rep(" ", targetLength - len) .. input
|
{ "config", "minimapTagger", "helpSoundFile" },
|
||||||
end
|
"MedicGangsterParadise.ogg"
|
||||||
end
|
),
|
||||||
return input
|
helpSoundThrottle = shared.GetOrDefault(
|
||||||
end
|
Heimdall_Data,
|
||||||
|
{ "config", "minimapTagger", "helpSoundThrottle" },
|
||||||
data.Whoer.Init()
|
2
|
||||||
data.Messenger.Init()
|
),
|
||||||
data.Spotter.Init()
|
helpTextureFile = shared.GetOrDefault(
|
||||||
data.DeathReporter.Init()
|
Heimdall_Data,
|
||||||
print("Heimdall loaded!")
|
{ "config", "minimapTagger", "helpTextureFile" },
|
||||||
end
|
"Aura68.tga"
|
||||||
|
),
|
||||||
local loadedFrame = CreateFrame("Frame")
|
},
|
||||||
loadedFrame:RegisterEvent("ADDON_LOADED")
|
whisperNotify = shared.GetOrDefault(Heimdall_Data, { "config", "whisperNotify" }, {}),
|
||||||
loadedFrame:SetScript("OnEvent", function(self, event, addonName)
|
stinkies = shared.GetOrDefault(Heimdall_Data, { "config", "stinkies" }, {}),
|
||||||
if addonName == addonname then
|
notes = shared.GetOrDefault(Heimdall_Data, { "config", "notes" }, {}),
|
||||||
init()
|
scale = shared.GetOrDefault(Heimdall_Data, { "config", "scale" }, 1),
|
||||||
end
|
locale = shared.GetOrDefault(Heimdall_Data, { "config", "locale" }, "en"),
|
||||||
end)
|
bonkDetector = {
|
||||||
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "bonkDetector", "enabled" }, false),
|
||||||
local logoutFrame = CreateFrame("Frame")
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "bonkDetector", "debug" }, false),
|
||||||
logoutFrame:RegisterEvent("PLAYER_LOGOUT")
|
channels = shared.GetOrDefault(Heimdall_Data, { "config", "bonkDetector", "channels" }, { "Agent" }),
|
||||||
logoutFrame:SetScript("OnEvent", function(self, event)
|
throttle = shared.GetOrDefault(Heimdall_Data, { "config", "bonkDetector", "throttle" }, 5),
|
||||||
Heimdall_Data.config.stinkies = data.config.stinkies
|
},
|
||||||
end)
|
noter = {
|
||||||
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "noter", "enabled" }, false),
|
||||||
SlashCmdList["HEIMDALL_TOGGLE_STINKY"] = function(input)
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "noter", "debug" }, false),
|
||||||
print("Toggling stinky: " .. tostring(input))
|
channels = shared.GetOrDefault(Heimdall_Data, { "config", "noter", "channels" }, { "Agent" }),
|
||||||
if data.config.stinkies[input] then
|
lastNotes = shared.GetOrDefault(Heimdall_Data, { "config", "noter", "lastNotes" }, 5),
|
||||||
data.config.stinkies[input] = nil
|
},
|
||||||
else
|
network = {
|
||||||
data.config.stinkies[input] = true
|
enabled = shared.GetOrDefault(Heimdall_Data, { "config", "network", "enabled" }, false),
|
||||||
end
|
debug = shared.GetOrDefault(Heimdall_Data, { "config", "network", "debug" }, false),
|
||||||
print(data.config.stinkies[input])
|
members = shared.GetOrDefault(Heimdall_Data, { "config", "network", "members" }, {}),
|
||||||
end
|
updateInterval = shared.GetOrDefault(Heimdall_Data, { "config", "network", "updateInterval" }, 10),
|
||||||
SLASH_HEIMDALL_TOGGLE_STINKY1 = "/has"
|
},
|
||||||
|
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" }, {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
shared.raceMap = {
|
||||||
|
["Orc"] = "Horde",
|
||||||
|
["Undead"] = "Horde",
|
||||||
|
["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",
|
||||||
|
}
|
||||||
|
|
||||||
|
shared.classColors = {
|
||||||
|
["Warrior"] = "C69B6D",
|
||||||
|
["Paladin"] = "F48CBA",
|
||||||
|
["Hunter"] = "AAD372",
|
||||||
|
["Rogue"] = "FFF468",
|
||||||
|
["Priest"] = "FFFFFF",
|
||||||
|
["Death Knight"] = "C41E3A",
|
||||||
|
["Shaman"] = "0070DD",
|
||||||
|
["Mage"] = "3FC7EB",
|
||||||
|
["Warlock"] = "8788EE",
|
||||||
|
["Monk"] = "00FF98",
|
||||||
|
["Druid"] = "FF7C0A",
|
||||||
|
["Demon Hunter"] = "A330C9",
|
||||||
|
}
|
||||||
|
|
||||||
|
---@param input string
|
||||||
|
---@return number
|
||||||
|
shared.utf8len = function(input)
|
||||||
|
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
|
||||||
|
|
||||||
|
---@param input string
|
||||||
|
---@param deliminer string
|
||||||
|
---@return table<number, string>
|
||||||
|
shared.Split = function(input, deliminer)
|
||||||
|
if deliminer == nil then deliminer = "%s" end
|
||||||
|
local t = {}
|
||||||
|
for str in string.gmatch(input, "([^" .. deliminer .. "]+)") do
|
||||||
|
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
|
||||||
|
|
||||||
|
---@param f function
|
||||||
|
---@return function
|
||||||
|
shared.Memoize = function(f)
|
||||||
|
local mem = {} -- memoizing table
|
||||||
|
setmetatable(mem, { __mode = "kv" }) -- make it weak
|
||||||
|
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
|
||||||
|
|
||||||
|
---@param channel string
|
||||||
|
---@return string
|
||||||
|
shared.GetLocaleForChannel = function(channel) return Heimdall_Data.config.channelLocale[channel] or "en" end
|
||||||
|
|
||||||
|
---@param key string
|
||||||
|
---@param locale string
|
||||||
|
---@return string
|
||||||
|
shared._L = function(key, locale)
|
||||||
|
local localeTable = shared._Locale[locale]
|
||||||
|
if not localeTable then
|
||||||
|
if Heimdall_Data.config.debug then
|
||||||
|
print(string.format("[Heimdall] Locale %s not found", tostring(locale)))
|
||||||
|
end
|
||||||
|
return key
|
||||||
|
end
|
||||||
|
local value = localeTable[key]
|
||||||
|
if not value then
|
||||||
|
if Heimdall_Data.config.debug then
|
||||||
|
print(string.format("[Heimdall] Key %s not found in locale %s", tostring(key), tostring(locale)))
|
||||||
|
end
|
||||||
|
return key
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
shared.Messenger.Init()
|
||||||
|
shared.StinkyTracker.Init()
|
||||||
|
shared.AgentTracker.Init()
|
||||||
|
shared.Whoer.Init()
|
||||||
|
shared.Spotter.Init()
|
||||||
|
shared.DeathReporter.Init()
|
||||||
|
shared.Inviter.Init()
|
||||||
|
shared.Dueler.Init()
|
||||||
|
shared.Bully.Init()
|
||||||
|
shared.Macroer.Init()
|
||||||
|
shared.Commander.Init()
|
||||||
|
shared.CombatAlerter.Init()
|
||||||
|
shared.Config.Init()
|
||||||
|
shared.MinimapTagger.Init()
|
||||||
|
shared.BonkDetector.Init()
|
||||||
|
shared.Sniffer.Init()
|
||||||
|
shared.Noter.Init()
|
||||||
|
shared.Network.Init()
|
||||||
|
shared.NetworkMessenger.Init()
|
||||||
|
shared.Configurator.Init()
|
||||||
|
shared.StinkyCache.Init()
|
||||||
|
shared.AchievementSniffer.Init()
|
||||||
|
shared.ChatSniffer.Init()
|
||||||
|
print("Heimdall loaded!")
|
||||||
|
end
|
||||||
|
|
||||||
|
local loadedFrame = CreateFrame("Frame")
|
||||||
|
loadedFrame:RegisterEvent("ADDON_LOADED")
|
||||||
|
loadedFrame:SetScript("OnEvent", function(self, event, addonName)
|
||||||
|
if addonName == addonname then init() end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Create the import/export frame
|
||||||
|
local ccpFrame = CreateFrame("Frame", "CCPFrame", UIParent)
|
||||||
|
ccpFrame:SetSize(512 * 1.5, 512 * 1.5)
|
||||||
|
ccpFrame:SetPoint("CENTER")
|
||||||
|
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)
|
||||||
|
|
||||||
|
ccpFrame:SetMovable(true)
|
||||||
|
ccpFrame:EnableMouse(true)
|
||||||
|
ccpFrame:RegisterForDrag("LeftButton")
|
||||||
|
ccpFrame:SetScript("OnDragStart", function(self) self:StartMoving() end)
|
||||||
|
ccpFrame:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)
|
||||||
|
ccpFrame:SetScript("OnShow", function(self) self:SetScale(1) end)
|
||||||
|
ccpFrame:Hide()
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|||||||
51
Heimdall.toc
51
Heimdall.toc
@@ -1,14 +1,37 @@
|
|||||||
## Interface: 70300
|
## Interface: 70300
|
||||||
## Title: Heimdall
|
## Title: Heimdall
|
||||||
## Notes: Watches over areas and alerts when hostiles spotted
|
## Version: 3.12.0
|
||||||
## Author: Cyka
|
## Notes: Watches over areas and alerts when hostiles spotted
|
||||||
## SavedVariables: Heimdall_Data
|
## Author: Cyka
|
||||||
|
## SavedVariables: Heimdall_Data, Heimdall_Achievements, Heimdall_Chat
|
||||||
#core
|
|
||||||
CLEUParser.lua
|
_L.lua
|
||||||
DumpTable.lua
|
Modules/CLEUParser.lua
|
||||||
Spotter.lua
|
Modules/ReactiveValue.lua
|
||||||
Whoer.lua
|
Modules/DumpTable.lua
|
||||||
Messenger.lua
|
Modules/Spotter.lua
|
||||||
DeathReporter.lua
|
Modules/Whoer.lua
|
||||||
Heimdall.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
|
||||||
|
|||||||
BIN
Heimdall.zip
LFS
Normal file
BIN
Heimdall.zip
LFS
Normal file
Binary file not shown.
100
Messenger.lua
100
Messenger.lua
@@ -1,100 +0,0 @@
|
|||||||
local addonname, data = ...
|
|
||||||
---@cast data HeimdallData
|
|
||||||
---@cast addonname string
|
|
||||||
|
|
||||||
data.Messenger = {}
|
|
||||||
function data.Messenger.Init()
|
|
||||||
if not data.config.messenger.enabled then
|
|
||||||
print("Heimdall - Messenger disabled")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class Message
|
|
||||||
---@field message string
|
|
||||||
---@field channel string
|
|
||||||
---@field data string
|
|
||||||
|
|
||||||
---@type table<string, number>
|
|
||||||
local channelIdMap = {}
|
|
||||||
|
|
||||||
local FindOrJoinChannel = function(channelName, password)
|
|
||||||
local function GetChannelId(channelName)
|
|
||||||
local channels = { GetChannelList() }
|
|
||||||
for i = 1, #channels, 2 do
|
|
||||||
local id = channels[i]
|
|
||||||
local name = channels[i + 1]
|
|
||||||
if name == channelName then
|
|
||||||
return id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local channelId = GetChannelId(channelName)
|
|
||||||
if not channelId then
|
|
||||||
print("Channel", tostring(channelName), "not found, joining")
|
|
||||||
if password then
|
|
||||||
JoinPermanentChannel(channelName, password)
|
|
||||||
else
|
|
||||||
JoinPermanentChannel(channelName)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
channelId = GetChannelId(channelName)
|
|
||||||
channelIdMap[channelName] = channelId
|
|
||||||
return channelId
|
|
||||||
end
|
|
||||||
|
|
||||||
local ScanChannels = function()
|
|
||||||
local channels = { GetChannelList() }
|
|
||||||
for i = 1, #channels, 2 do
|
|
||||||
local id = channels[i]
|
|
||||||
local name = channels[i + 1]
|
|
||||||
channelIdMap[name] = id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not data.messenger then data.messenger = {} end
|
|
||||||
if not data.messenger.queue then data.messenger.queue = {} end
|
|
||||||
if not data.messenger.ticker then
|
|
||||||
data.messenger.ticker = C_Timer.NewTicker(0.2, function()
|
|
||||||
---@type Message
|
|
||||||
local message = data.messenger.queue[1]
|
|
||||||
if not message then return end
|
|
||||||
if not message.message or message.message == "" then return end
|
|
||||||
if not message.channel or message.channel == "" then return end
|
|
||||||
|
|
||||||
-- Map channel names to ids
|
|
||||||
if message.channel == "CHANNEL" and message.data and string.match(message.data, "%D") then
|
|
||||||
print("Channel presented as string:", message.data)
|
|
||||||
local channelId = channelIdMap[message.data]
|
|
||||||
if not channelId then
|
|
||||||
print("Channel not found, scanning")
|
|
||||||
ScanChannels()
|
|
||||||
channelId = channelIdMap[message.data]
|
|
||||||
end
|
|
||||||
if not channelId then
|
|
||||||
print("Channel not joined, joining")
|
|
||||||
channelId = FindOrJoinChannel(message.data)
|
|
||||||
end
|
|
||||||
print("Channel resolved to id", channelId)
|
|
||||||
message.data = channelId
|
|
||||||
end
|
|
||||||
|
|
||||||
table.remove(data.messenger.queue, 1)
|
|
||||||
if not message.message or message.message == "" then return end
|
|
||||||
if not message.channel or message.channel == "" then return end
|
|
||||||
if not message.data or message.data == "" then return end
|
|
||||||
SendChatMessage(message.message, message.channel, nil, message.data)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--C_Timer.NewTicker(2, function()
|
|
||||||
-- print("Q")
|
|
||||||
-- table.insert(data.messenger.queue, {
|
|
||||||
-- channel = "CHANNEL",
|
|
||||||
-- data = "Foobar",
|
|
||||||
-- message = "TEST"
|
|
||||||
-- })
|
|
||||||
--end)
|
|
||||||
|
|
||||||
print("Heimdall - Messenger loaded")
|
|
||||||
end
|
|
||||||
1
Meta
Submodule
1
Meta
Submodule
Submodule Meta added at eee043a846
304
Modules/AchievementSniffer.lua
Normal file
304
Modules/AchievementSniffer.lua
Normal 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
144
Modules/AgentTracker.lua
Normal 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
142
Modules/BonkDetector.lua
Normal 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
16
Modules/Bully.lua
Normal 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,
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
49
Modules/ChatSniffer.lua
Normal file
49
Modules/ChatSniffer.lua
Normal 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
141
Modules/CombatAlerter.lua
Normal 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
438
Modules/Commander.lua
Normal 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
2710
Modules/Config.lua
Normal file
File diff suppressed because it is too large
Load Diff
12
Modules/Configurator.lua
Normal file
12
Modules/Configurator.lua
Normal 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
268
Modules/DeathReporter.lua
Normal 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
63
Modules/Dueler.lua
Normal 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
32
Modules/DumpTable.lua
Normal 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
65
Modules/Echoer.lua
Normal 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
66
Modules/Emoter.lua
Normal 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
275
Modules/Inviter.lua
Normal 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
101
Modules/Macroer.lua
Normal 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
176
Modules/Messenger.lua
Normal 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
597
Modules/MinimapTagger.lua
Normal 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
85
Modules/Network.lua
Normal 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,
|
||||||
|
}
|
||||||
204
Modules/NetworkMessenger.lua
Normal file
204
Modules/NetworkMessenger.lua
Normal 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
307
Modules/Noter.lua
Normal 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
609
Modules/ReactiveValue.lua
Normal 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
92
Modules/Sniffer.lua
Normal 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
238
Modules/Spotter.lua
Normal 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
83
Modules/StinkyCache.lua
Normal 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
364
Modules/StinkyTracker.lua
Normal 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
735
Modules/Whoer.lua
Normal 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
324
README.md
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
# Heimdall WoW Addon
|
||||||
|
|
||||||
|
Heimdall is a comprehensive World of Warcraft addon designed to provide advanced player tracking, notification, and group management features.
|
||||||
|
|
||||||
|
## Report overview
|
||||||
|
|
||||||
|
- Player spotted:
|
||||||
|
- "I see (Hostile) Atomickitty of race Blood Elf (Horde) with health 6.8M/6.8M at Orgrimmar (Valley of Strength)"
|
||||||
|
- "I see (\<reaction\>) \<name\> of race \<race\> (\<faction\>) with health \<health\>/\<healthMax\> at \<location\>"
|
||||||
|
- Player appeared:
|
||||||
|
- "Любящаядева of class Mage, race Human (Alliance) and guild Анонимное сообщество in Valley of Trials, first seen: 2024-12-27T15:54:38, last seen: never, times seen: 0"
|
||||||
|
- "\<name\> of class \<class\>, race \<race\> (\<faction\>) and guild \<guild\> in \<zone\>, first seen: \<firstSeen\>, last seen: \<lastSeen\>, times seen: \<timesSeen\>"
|
||||||
|
- Player changed zone:
|
||||||
|
- "Thekinglord of class Paladin (Human - Alliance) and guild Caminantes Nocturnos R moved to Durotar"
|
||||||
|
- "\<name\> of class \<class\> (\<faction\>) and guild \<guild\> moved to \<zone\>"
|
||||||
|
- Player disappeared:
|
||||||
|
- "Thekinglord of class Paladin and guild Caminantes Nocturnos R left Valley of Trials"
|
||||||
|
- "\<name\> of class \<class\> and guild \<guild\> left \<zone\>"
|
||||||
|
- Player killed:
|
||||||
|
- "Euotuie killed Wild Mature Swine with Demon's Bite in Durotar (The Dranosh'ar Blockade)"
|
||||||
|
- "\<killer\> killed \<victim\> with \<spell\> in \<zone\> (\<subzone\>)"
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Heimdall is a multi-module addon that offers various functionalities to enhance player interaction and awareness in the game. It consists of several key modules, each with a specific purpose:
|
||||||
|
|
||||||
|
### 1. Spotter Module (`Spotter.lua`)
|
||||||
|
- Tracks and reports player sightings in real-time
|
||||||
|
- Configurable notification settings:
|
||||||
|
- Detect players by faction (Alliance, Horde)
|
||||||
|
- Identify hostile players
|
||||||
|
- Mark "stinky" players (predefined list)
|
||||||
|
- Sends notifications to a specified channel when players are spotted
|
||||||
|
- Provides detailed information about spotted players:
|
||||||
|
- Name
|
||||||
|
- Race
|
||||||
|
- Faction
|
||||||
|
- Health
|
||||||
|
- Location
|
||||||
|
- **Example report:**
|
||||||
|
- "I see (Hostile) Atomickitty of race Blood Elf (Horde) with health 6.8M/6.8M at Orgrimmar (Valley of Strength)"
|
||||||
|
- "I see (\<reaction\>) \<name\> of race \<race\> (\<faction\>) with health \<health\>/\<healthMax\> at \<location\>"
|
||||||
|
- Configuration:
|
||||||
|
- `enabled` - Whether the module is enabled
|
||||||
|
- `everyone` - Whether to report to everyone in the channel
|
||||||
|
- `hostile` - Whether to report hostile players (regardless of faction, ie. when a horde becomes alliance)
|
||||||
|
- `alliance` - Whether to report alliance players
|
||||||
|
- `stinky` - Whether to report only stinky players
|
||||||
|
- `notifyChannel` - The channel to report to (by name)
|
||||||
|
- `zoneOverride` - The zone to override the zone of the player to report (defaults to current zone/subzone)
|
||||||
|
- `throttleTime` - The time to throttle the reports to (in seconds)
|
||||||
|
- Configuration example:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.spotter = {enabled=true,everyone=false,hostile=true,alliance=true,stinky=true,notifyChannel="Agent",zoneOverride=nil,throttleTime=10}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Whoer Module (`Whoer.lua`)
|
||||||
|
- Advanced player tracking and logging system
|
||||||
|
- Periodically performs WHO queries in specific zones
|
||||||
|
- Maintains a persistent database of player information:
|
||||||
|
- First seen
|
||||||
|
- Last seen
|
||||||
|
- Seen count
|
||||||
|
- Zone history
|
||||||
|
- Sends notifications when:
|
||||||
|
- New players are detected
|
||||||
|
- Players change zones
|
||||||
|
- Players disappear from tracking
|
||||||
|
- Supports whisper notifications to predefined contacts
|
||||||
|
- Plays sound alerts for "stinky" players
|
||||||
|
- **Example report:**
|
||||||
|
- Player appeared:
|
||||||
|
- "Любящаядева of class Mage, race Human (Alliance) and guild Анонимное сообщество in Valley of Trials, first seen: 2024-12-27T15:54:38, last seen: never, times seen: 0"
|
||||||
|
- "\<name\> of class \<class\>, race \<race\> (\<faction\>) and guild \<guild\> in \<zone\>, first seen: \<firstSeen\>, last seen: \<lastSeen\>, times seen: \<timesSeen\>"
|
||||||
|
- Player changed zone:
|
||||||
|
- "Thekinglord of class Paladin (Human - Alliance) and guild Caminantes Nocturnos R moved to Durotar"
|
||||||
|
- "\<name\> of class \<class\> (\<faction\>) and guild \<guild\> moved to \<zone\>"
|
||||||
|
- Player disappeared:
|
||||||
|
- "Thekinglord of class Paladin and guild Caminantes Nocturnos R left Valley of Trials"
|
||||||
|
- "\<name\> of class \<class\> and guild \<guild\> left \<zone\>"
|
||||||
|
- Configuration:
|
||||||
|
- `enabled` - Whether the module is enabled
|
||||||
|
- `notifyChannel` - The channel to report to (by name)
|
||||||
|
- `ttl` - The time to live for a player (in seconds)
|
||||||
|
- `doWhisper` - Whether to whisper to predefined contacts
|
||||||
|
- `zoneNotifyFor` - Whether to notify for players in specific zones
|
||||||
|
- Configuration example:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.who = {enabled=true,notifyChannel="Agent",ttl=20,doWhisper=true,zoneNotifyFor={["Orgrimmar"]=true,["Thunder Bluff"]=true,["Undercity"]=true,["Durotar"]=true,["Echo Isles"]=true,["Valley of Trials"]=true}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Messenger Module (`Messenger.lua`)
|
||||||
|
- Centralized message queuing and sending system
|
||||||
|
- Manages message delivery across different chat channels
|
||||||
|
- Handles channel joining and message routing
|
||||||
|
- Provides a reliable messaging infrastructure for other modules
|
||||||
|
- Configuration:
|
||||||
|
- `enabled` - Whether the module is enabled
|
||||||
|
- Configuration example:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.messenger = {enabled=true}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Inviter Module (`Inviter.lua`)
|
||||||
|
- Automated group invitation system
|
||||||
|
- Listens to a specific channel for invitation requests
|
||||||
|
- Supports a configurable keyword for invitations
|
||||||
|
- Automatically promotes channel members to assistants in raid groups
|
||||||
|
- Configuration:
|
||||||
|
- `enabled` - Whether the module is enabled
|
||||||
|
- `keyword` - The keyword to listen for
|
||||||
|
- `updateInterval` - The interval to update the list of channel members (in seconds)
|
||||||
|
- `listeningChannel` - The channel to listen for invitations
|
||||||
|
- Configuration example:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.inviter = {enabled=true,keyword="+",updateInterval=10,listeningChannel="Agent"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Death Reporter Module (`DeathReporter.lua`)
|
||||||
|
- Tracks and reports player deaths in combat
|
||||||
|
- Captures detailed death information:
|
||||||
|
- Killer
|
||||||
|
- Victim
|
||||||
|
- Killing spell
|
||||||
|
- Location
|
||||||
|
- Implements throttling to prevent spam
|
||||||
|
- Handles duel detection to avoid reporting duel-related deaths
|
||||||
|
- Sends notifications to a specified channel and optional whisper contacts
|
||||||
|
- **Example report:**
|
||||||
|
- "Euotuie killed Wild Mature Swine with Demon's Bite in Durotar (The Dranosh'ar Blockade)"
|
||||||
|
- "\<killer\> killed \<victim\> with \<spell\> in \<zone\> (\<subzone\>)"
|
||||||
|
- Configuration:
|
||||||
|
- `enabled` - Whether the module is enabled
|
||||||
|
- `notifyChannel` - The channel to report to (by name)
|
||||||
|
- `doWhisper` - Whether to whisper to predefined contacts
|
||||||
|
- Configuration example:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.deathReporter = {enabled=true,notifyChannel="Agent",doWhisper=true}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Core Module (`Heimdall.lua`)
|
||||||
|
- Initializes and configures all other modules
|
||||||
|
- Manages global configuration and data persistence
|
||||||
|
- Provides utility functions for:
|
||||||
|
- UTF-8 string handling
|
||||||
|
- String padding
|
||||||
|
- Data retrieval with defaults
|
||||||
|
|
||||||
|
## Stinky Players
|
||||||
|
|
||||||
|
The addon maintains a list of "stinky" players - users of interest that trigger special notifications and tracking.
|
||||||
|
|
||||||
|
## Slash Commands
|
||||||
|
|
||||||
|
- `/has [PlayerName]`: Toggle a player's "stinky" status
|
||||||
|
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
1. Download the [addon](https://git.site.quack-lab.dev/dave/wow-Heimdall/media/branch/master/Heimdall.zip)
|
||||||
|
2. Extract the addon to your World of Warcraft `Interface/AddOns` directory
|
||||||
|
3. Ensure the addon is enabled in the character selection screen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Аддон Heimdall для WoW
|
||||||
|
|
||||||
|
Heimdall - это комплексный аддон для World of Warcraft, предназначенный для расширенного отслеживания игроков, уведомлений и управления группами.
|
||||||
|
|
||||||
|
## Обзор отчетов
|
||||||
|
|
||||||
|
- Обнаружен игрок:
|
||||||
|
- "I see (Hostile) Atomickitty of race Blood Elf (Horde) with health 6.8M/6.8M at Orgrimmar (Valley of Strength)"
|
||||||
|
- "Я вижу (\<reaction\>) \<name\> расы \<race\> (\<faction\>) со здоровьем \<health\>/\<healthMax\> в \<location\>"
|
||||||
|
- Появился игрок:
|
||||||
|
- "Любящаядева of class Mage, race Human (Alliance) and guild Анонимное сообщество in Valley of Trials, first seen: 2024-12-27T15:54:38, last seen: never, times seen: 0"
|
||||||
|
- "\<name\> класса \<class\>, расы \<race\> (\<faction\>) и гильдии \<guild\> в \<zone\>, первый раз замечен: \<firstSeen\>, последний раз замечен: \<lastSeen\>, встречен раз: \<timesSeen\>"
|
||||||
|
- Игрок сменил зону:
|
||||||
|
- "Thekinglord of class Paladin (Human - Alliance) and guild Caminantes Nocturnos R moved to Durotar"
|
||||||
|
- "\<name\> класса \<class\> (\<faction\>) и гильдии \<guild\> перешёл в \<zone\>"
|
||||||
|
- Игрок исчез:
|
||||||
|
- "Thekinglord of class Paladin and guild Caminantes Nocturnos R left Valley of Trials"
|
||||||
|
- "\<name\> класса \<class\> и гильдии \<guild\> покинул \<zone\>"
|
||||||
|
- Игрок убит:
|
||||||
|
- "Euotuie killed Wild Mature Swine with Demon's Bite in Durotar (The Dranosh'ar Blockade)"
|
||||||
|
- "\<killer\> убил \<victim\> с помощью \<spell\> в \<zone\> (\<subzone\>)"
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
Heimdall - это многомодульный аддон, предоставляющий различные функции для улучшения взаимодействия между игроками и повышения осведомленности в игре. Он состоит из нескольких ключевых модулей:
|
||||||
|
|
||||||
|
### 1. Модуль Обнаружения (`Spotter.lua`)
|
||||||
|
- Отслеживает и сообщает об обнаружении игроков в реальном времени
|
||||||
|
- Настраиваемые параметры уведомлений:
|
||||||
|
- Обнаружение игроков по фракции (Альянс, Орда)
|
||||||
|
- Определение враждебных игроков
|
||||||
|
- Отметка "подозрительных" игроков (предопределенный список)
|
||||||
|
- Отправляет уведомления в указанный канал при обнаружении игроков
|
||||||
|
- Предоставляет подробную информацию об обнаруженных игроках:
|
||||||
|
- Имя
|
||||||
|
- Раса
|
||||||
|
- Фракция
|
||||||
|
- Здоровье
|
||||||
|
- Местоположение
|
||||||
|
- **Пример отчета:**
|
||||||
|
- "I see (Hostile) Atomickitty of race Blood Elf (Horde) with health 6.8M/6.8M at Orgrimmar (Valley of Strength)"
|
||||||
|
- "Обнаружен (\<реакция\>) \<имя\> расы \<раса\> (\<фракция\>) со здоровьем \<здоровье\>/\<макс_здоровье\> в \<локация\>"
|
||||||
|
- Конфигурация:
|
||||||
|
- `enabled` - Включен ли модуль
|
||||||
|
- `everyone` - Сообщать ли всем в канале
|
||||||
|
- `hostile` - Сообщать ли о враждебных игроках
|
||||||
|
- `alliance` - Сообщать ли об игроках Альянса
|
||||||
|
- `stinky` - Сообщать ли только о подозрительных игроках
|
||||||
|
- `notifyChannel` - Канал для отправки сообщений (по имени)
|
||||||
|
- `zoneOverride` - Зона для переопределения местоположения игрока
|
||||||
|
- `throttleTime` - Время задержки между сообщениями (в секундах)
|
||||||
|
- Пример конфигурации:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.spotter = {enabled=true,everyone=false,hostile=true,alliance=true,stinky=true,notifyChannel="Agent",zoneOverride=nil,throttleTime=10}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Модуль WHO (`Whoer.lua`)
|
||||||
|
- Продвинутая система отслеживания и логирования игроков
|
||||||
|
- Периодически выполняет WHO запросы в определенных зонах
|
||||||
|
- Поддерживает постоянную базу данных информации об игроках:
|
||||||
|
- Первое появление
|
||||||
|
- Последнее появление
|
||||||
|
- Количество появлений
|
||||||
|
- История зон
|
||||||
|
- Отправляет уведомления когда:
|
||||||
|
- Обнаружены новые игроки
|
||||||
|
- Игроки меняют зоны
|
||||||
|
- Игроки исчезают из отслеживания
|
||||||
|
- Поддерживает уведомления шепотом предопределенным контактам
|
||||||
|
- Проигрывает звуковые оповещения для "подозрительных" игроков
|
||||||
|
- **Пример отчета:**
|
||||||
|
- Появление игрока:
|
||||||
|
- "Любящаядева of class Mage, race Human (Alliance) and guild Анонимное сообщество in Valley of Trials, first seen: 2024-12-27T15:54:38, last seen: never, times seen: 0"
|
||||||
|
- "Имя класса <класс>, расы <раса> (<фракция>) из гильдии <гильдия> в <зона>, первое появление: <первое_появление>, последнее появление: <последнее_появление>, появлений: <количество>"
|
||||||
|
- Смена зоны:
|
||||||
|
- "Thekinglord of class Paladin (Human - Alliance) and guild Caminantes Nocturnos R moved to Durotar"
|
||||||
|
- "<имя> класса <класс> (<фракция>) из гильдии <гильдия> переместился в <зона>"
|
||||||
|
- Исчезновение:
|
||||||
|
- "Thekinglord of class Paladin and guild Caminantes Nocturnos R left Valley of Trials"
|
||||||
|
- "<имя> класса <класс> из гильдии <гильдия> покинул <зона>"
|
||||||
|
- Конфигурация:
|
||||||
|
- `enabled` - Включен ли модуль
|
||||||
|
- `notifyChannel` - Канал для отправки сообщений
|
||||||
|
- `ttl` - Время жизни записи об игроке (в секундах)
|
||||||
|
- `doWhisper` - Отправлять ли шепот контактам
|
||||||
|
- `zoneNotifyFor` - Уведомлять ли об игроках в определенных зонах
|
||||||
|
- Пример конфигурации:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.who = {enabled=true,notifyChannel="Agent",ttl=20,doWhisper=true,zoneNotifyFor={["Orgrimmar"]=true,["Thunder Bluff"]=true,["Undercity"]=true,["Durotar"]=true,["Echo Isles"]=true,["Valley of Trials"]=true}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Модуль Сообщений (`Messenger.lua`)
|
||||||
|
- Централизованная система очередей и отправки сообщений
|
||||||
|
- Управляет доставкой сообщений по разным чат-каналам
|
||||||
|
- Обрабатывает присоединение к каналам и маршрутизацию сообщений
|
||||||
|
- Предоставляет надежную инфраструктуру сообщений для других модулей
|
||||||
|
- Конфигурация:
|
||||||
|
- `enabled` - Включен ли модуль
|
||||||
|
- Пример конфигурации:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.messenger = {enabled=true}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Модуль Приглашений (`Inviter.lua`)
|
||||||
|
- Автоматическая система приглашений в группу
|
||||||
|
- Прослушивает определенный канал на запросы приглашений
|
||||||
|
- Поддерживает настраиваемое ключевое слово для приглашений
|
||||||
|
- Автоматически повышает участников канала до помощников в рейдовых группах
|
||||||
|
- Конфигурация:
|
||||||
|
- `enabled` - Включен ли модуль
|
||||||
|
- `keyword` - Ключевое слово для прослушивания
|
||||||
|
- `updateInterval` - Интервал обновления списка участников канала (в секундах)
|
||||||
|
- `listeningChannel` - Канал для прослушивания приглашений
|
||||||
|
- Пример конфигурации:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.inviter = {enabled=true,keyword="+",updateInterval=10,listeningChannel="Agent"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Модуль Отчетов о Смертях (`DeathReporter.lua`)
|
||||||
|
- Отслеживает и сообщает о смертях игроков в бою
|
||||||
|
- Сохраняет подробную информацию о смерти:
|
||||||
|
- Убийца
|
||||||
|
- Жертва
|
||||||
|
- Убивающее заклинание
|
||||||
|
- Местоположение
|
||||||
|
- Реализует задержку для предотвращения спама
|
||||||
|
- Обрабатывает определение дуэлей во избежание сообщений о смертях в дуэлях
|
||||||
|
- Отправляет уведомления в указанный канал и опционально шепотом контактам
|
||||||
|
- **Пример отчета:**
|
||||||
|
- "Euotuie killed Wild Mature Swine with Demon's Bite in Durotar (The Dranosh'ar Blockade)"
|
||||||
|
- "<убийца> убил <жертва> с помощью <заклинание> в <зона> (<подзона>)"
|
||||||
|
- Конфигурация:
|
||||||
|
- `enabled` - Включен ли модуль
|
||||||
|
- `notifyChannel` - Канал для отправки сообщений
|
||||||
|
- `doWhisper` - Отправлять ли шепот контактам
|
||||||
|
- Пример конфигурации:
|
||||||
|
```
|
||||||
|
/run Heimdall_Data.config.deathReporter = {enabled=true,notifyChannel="Agent",doWhisper=true}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Основной Модуль (`Heimdall.lua`)
|
||||||
|
- Инициализирует и настраивает все остальные модули
|
||||||
|
- Управляет глобальной конфигурацией и сохранением данных
|
||||||
|
- Предоставляет служебные функции для:
|
||||||
|
- Обработки UTF-8 строк
|
||||||
|
- Выравнивания строк
|
||||||
|
- Получения данных с значениями по умолчанию
|
||||||
|
|
||||||
|
## Подозрительные Игроки
|
||||||
|
|
||||||
|
Аддон поддерживает список "подозрительных" игроков - пользователей, представляющих интерес, которые вызывают специальные уведомления и отслеживание.
|
||||||
|
|
||||||
|
## Слэш-команды
|
||||||
|
|
||||||
|
- `/has [ИмяИгрока]`: Переключить статус "подозрительного" игрока
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
1. Скачайте [аддон](https://git.site.quack-lab.dev/dave/wow-Heimdall/media/branch/master/Heimdall.zip)
|
||||||
|
2. Распакуйте аддон в директорию World of Warcraft `Interface/AddOns`
|
||||||
|
3. Убедитесь, что аддон включен на экране выбора персонажа
|
||||||
BIN
Sounds/MGSSpot.ogg
LFS
Normal file
BIN
Sounds/MGSSpot.ogg
LFS
Normal file
Binary file not shown.
BIN
Sounds/MedicGangsterParadise.ogg
LFS
Normal file
BIN
Sounds/MedicGangsterParadise.ogg
LFS
Normal file
Binary file not shown.
BIN
Sounds/OOF.ogg
LFS
Normal file
BIN
Sounds/OOF.ogg
LFS
Normal file
Binary file not shown.
BIN
Sounds/StarScream.ogg
LFS
Normal file
BIN
Sounds/StarScream.ogg
LFS
Normal file
Binary file not shown.
117
Spotter.lua
117
Spotter.lua
@@ -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
LFS
Normal file
BIN
Texture/Aura1.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura10.tga
LFS
Normal file
BIN
Texture/Aura10.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura100.tga
LFS
Normal file
BIN
Texture/Aura100.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura101.tga
LFS
Normal file
BIN
Texture/Aura101.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura102.tga
LFS
Normal file
BIN
Texture/Aura102.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura103.tga
LFS
Normal file
BIN
Texture/Aura103.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura104.tga
LFS
Normal file
BIN
Texture/Aura104.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura105.tga
LFS
Normal file
BIN
Texture/Aura105.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura106.tga
LFS
Normal file
BIN
Texture/Aura106.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura107.tga
LFS
Normal file
BIN
Texture/Aura107.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura108.tga
LFS
Normal file
BIN
Texture/Aura108.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura109.tga
LFS
Normal file
BIN
Texture/Aura109.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura11.tga
LFS
Normal file
BIN
Texture/Aura11.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura110.tga
LFS
Normal file
BIN
Texture/Aura110.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura111.tga
LFS
Normal file
BIN
Texture/Aura111.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura112.tga
LFS
Normal file
BIN
Texture/Aura112.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura113.tga
LFS
Normal file
BIN
Texture/Aura113.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura114.tga
LFS
Normal file
BIN
Texture/Aura114.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura115.tga
LFS
Normal file
BIN
Texture/Aura115.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura116.tga
LFS
Normal file
BIN
Texture/Aura116.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura117.tga
LFS
Normal file
BIN
Texture/Aura117.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura118.tga
LFS
Normal file
BIN
Texture/Aura118.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura119.tga
LFS
Normal file
BIN
Texture/Aura119.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura12.tga
LFS
Normal file
BIN
Texture/Aura12.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura120.tga
LFS
Normal file
BIN
Texture/Aura120.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura121.tga
LFS
Normal file
BIN
Texture/Aura121.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura122.tga
LFS
Normal file
BIN
Texture/Aura122.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura123.tga
LFS
Normal file
BIN
Texture/Aura123.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura124.tga
LFS
Normal file
BIN
Texture/Aura124.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura125.tga
LFS
Normal file
BIN
Texture/Aura125.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura126.tga
LFS
Normal file
BIN
Texture/Aura126.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura127.tga
LFS
Normal file
BIN
Texture/Aura127.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura128.tga
LFS
Normal file
BIN
Texture/Aura128.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura129.tga
LFS
Normal file
BIN
Texture/Aura129.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura13.tga
LFS
Normal file
BIN
Texture/Aura13.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura130.tga
LFS
Normal file
BIN
Texture/Aura130.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura131.tga
LFS
Normal file
BIN
Texture/Aura131.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura132.tga
LFS
Normal file
BIN
Texture/Aura132.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura133.tga
LFS
Normal file
BIN
Texture/Aura133.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura134.tga
LFS
Normal file
BIN
Texture/Aura134.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura135.tga
LFS
Normal file
BIN
Texture/Aura135.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura136.tga
LFS
Normal file
BIN
Texture/Aura136.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura137.tga
LFS
Normal file
BIN
Texture/Aura137.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura138.tga
LFS
Normal file
BIN
Texture/Aura138.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura139.tga
LFS
Normal file
BIN
Texture/Aura139.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura14.tga
LFS
Normal file
BIN
Texture/Aura14.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura140.tga
LFS
Normal file
BIN
Texture/Aura140.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura141.tga
LFS
Normal file
BIN
Texture/Aura141.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura142.tga
LFS
Normal file
BIN
Texture/Aura142.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura143.tga
LFS
Normal file
BIN
Texture/Aura143.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura144.tga
LFS
Normal file
BIN
Texture/Aura144.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura145.tga
LFS
Normal file
BIN
Texture/Aura145.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura15.tga
LFS
Normal file
BIN
Texture/Aura15.tga
LFS
Normal file
Binary file not shown.
BIN
Texture/Aura16.tga
LFS
Normal file
BIN
Texture/Aura16.tga
LFS
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user