forked from iraizo/AutoLayer
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlayering.lua
More file actions
executable file
·433 lines (368 loc) · 15.4 KB
/
layering.lua
File metadata and controls
executable file
·433 lines (368 loc) · 15.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
local addonName, addonTable = ...;
local CTL = _G.ChatThrottleLib
local playersInvitedRecently = {}
local recentLayerRequests = {}
local kicked_player_queue = {}
--- @return boolean is_logging_out Whether the current player is logging out
local function isPlayerLoggingOut()
for i = 1, STATICPOPUP_NUMDIALOGS do
local frame = _G["StaticPopup" .. i]
if frame and frame:IsShown() and frame.which == "CAMP" then
return true
end
end
return false
end
function AutoLayer:pruneCache()
for i, cachedPlayer in ipairs(playersInvitedRecently) do
-- delete players that are over 5 minutes old
if cachedPlayer.time + 300 < time() then
self:DebugPrint("Removing ", cachedPlayer.name, " from players invited recently")
table.remove(playersInvitedRecently, i)
end
end
for i, cachedPlayer in ipairs(recentLayerRequests) do
-- delete players that are over 1 minute old
if cachedPlayer.time + 60 < time() then
self:DebugPrint("Removing ", cachedPlayer.name, " from recent layer requests")
table.remove(recentLayerRequests, i)
end
end
end
--- @param number number The number to check for in the list.
--- @param list table<number> The list of numbers to check against.
--- @return boolean is_in_list Whether the number is in the list.
local function isNumberInList(number, list)
for _, value in ipairs(list) do
if value == number then
return true
end
end
return false
end
local function removeRealmName(name)
return ({ strsplit("-", name) })[1]
end
--- Checks if a message contains any word from a given list, with an option to respect word boundaries.
--- @param msg string The message to search through.
--- @param listOfWords table<string> A list of words to search for in the message.
--- @param respectWordBoundaries boolean? (optional) Whether to respect word boundaries in the search. Defaults to true.
--- @return boolean The first word found in the message that matches a word from the list; false otherwise.
local function containsAnyWordFromList(msg, listOfWords, respectWordBoundaries)
-- Default to true if not explicitly set
respectWordBoundaries = respectWordBoundaries ~= false
local lowerMsg = string.lower(msg)
for _, word in ipairs(listOfWords) do
local lowerWord = string.lower(word)
local pattern
if respectWordBoundaries then
pattern = "%f[%a]" .. lowerWord .. "%f[%A]"
else
pattern = lowerWord
end
if string.find(lowerMsg, pattern) then
return word -- Return the matched word
end
end
return false -- Return false if nothing matched
end
--- Extracts unique, sorted layer numbers from a message.
--- Identifies individual and ranged layer numbers (e.g., "1", "1-3") in a message,
--- compiling them into a sorted list without duplicates.
---
--- @param message string The input string containing layer numbers.
--- @return table<number> layer_numbers List of sorted, unique layer numbers.
local function parseLayers(message)
local layers = {}
-- Add individual layers
for num in string.gmatch(message, "%d+") do
layers[#layers + 1] = tonumber(num)
end
-- Expand ranges, e.g. "layer 1-3" is the same as "layer 1,2,3"
for rangeStart, rangeEnd in string.gmatch(message, "(%d+)%-(%d+)") do
local startNum = tonumber(rangeStart)
local endNum = tonumber(rangeEnd)
-- but what if someone is a freak and says "layer 3-1" instead of "layer 1-3"?
if startNum > endNum then
startNum, endNum = endNum, startNum -- Swap values if out of order
end
for i = startNum, endNum do
layers[#layers + 1] = i
end
end
-- Sort layers
table.sort(layers)
-- Make a new list without duplicates (this code assumes the list is already sorted)
local uniqueLayers = {}
uniqueLayers[1] = layers[1]
for i = 2, #layers do
if layers[i] ~= layers[i - 1] then
uniqueLayers[#uniqueLayers + 1] = layers[i]
end
end
return uniqueLayers
end
function AutoLayer:ScanLayerFromNWB()
for name in LibStub("AceAddon-3.0"):IterateAddons() do
if name == "NovaWorldBuffs" then
addonTable.NWB = LibStub("AceAddon-3.0"):GetAddon("NovaWorldBuffs")
return
end
end
end
--- Finds the current layer from NovaWorldBuffs
--- @return number? layer The current layer number, or nil if the layer is unknown.
function AutoLayer:getCurrentLayer()
if addonTable.NWB == nil then return end -- No NWB, nothing to do here
-- If our layer is missing again, try to re-scan it once.
if addonTable.NWB.currentLayer == nil or addonTable.NWB.currentLayer <= 0 then
AutoLayer:ScanLayerFromNWB()
end
return tonumber(addonTable.NWB.currentLayer)
end
-- Autoexec?
C_Timer.After(0.1,
function()
AutoLayer:ScanLayerFromNWB()
if addonTable.NWB == nil then
AutoLayer:Print("Could not find NovaWorldBuffs, disabling NovaWorldBuffs integration")
end
end
)
function AutoLayer:FindOfflineMembersToKick()
for i = 1, GetNumGroupMembers() do
local name, _, _, _, _, _, _, online, _, _, _, _ = GetRaidRosterInfo(i)
if online == false then
table.insert(kicked_player_queue, name)
end
end
end
---@diagnostic disable-next-line:inject-field
function AutoLayer:ProcessMessage(event, msg, name)
if not self.db.profile.enabled or isPlayerLoggingOut() then
return
end
if event ~= "CHAT_MSG_GUILD" and self.db.profile.guildOnly then
return
end
local name_without_realm = removeRealmName(name)
if name_without_realm == UnitName("player") then
return
end
local triggerMatch = containsAnyWordFromList(msg, AutoLayer:ParseTriggers(), true)
if not triggerMatch then
return
end
local blacklistMatch = containsAnyWordFromList(msg, AutoLayer:ParseBlacklist(), false)
if blacklistMatch then
self:DebugPrint("Matched blacklist: '", blacklistMatch, "' in message: '", msg, "' from player '",
name_without_realm, "'")
return
end
-- If we got this far, we have a valid match.
self:DebugPrint("Matched trigger: '", triggerMatch, "' in message: '", msg, "' from player '", name_without_realm,
"'")
if self.db.profile.turnOffWhileRaidAssist and IsInRaid() and UnitIsGroupAssistant("player") then
self:DebugPrint("Ignoring request because we are raid assist!")
return
end
local currentLayer = AutoLayer:getCurrentLayer()
local isHighPriorityRequest = (event == "CHAT_MSG_WHISPER");
if string.find(msg, "%d+") then -- Uh oh, this player is picky and wants a specific layer!
if not currentLayer or currentLayer <= 0 then
self:DebugPrint("Message requested a specific layer, but we don't know what layer we're in! NWB says: ",
addonTable.NWB.currentLayer)
return
end
local requestedLayers = parseLayers(msg)
if not requestedLayers or next(requestedLayers) == nil then
self:DebugPrint("Message requested a specific layer, but we couldn't parse the message successfully!")
return
end
local requestIsInverted = containsAnyWordFromList(msg, AutoLayer:ParseInvertKeywords(), false)
local currLayerMatchesRequest = isNumberInList(currentLayer, requestedLayers)
if requestIsInverted then
self:DebugPrint("Message requested any layers except:", table.concat(requestedLayers, ", "))
else
self:DebugPrint("Message requested layers:", table.concat(requestedLayers, ", "))
end
if (requestIsInverted and currLayerMatchesRequest) or (not requestIsInverted and not currLayerMatchesRequest) then
self:DebugPrint("Request not satisfied. We are in layer ", currentLayer)
return
end
end
--If we got this far, then the message is a valid layer request that we can fulfill.
-- check if we've already invited this player in the last 5 minutes
if not isHighPriorityRequest then
AutoLayer:pruneCache()
for _, cachedPlayer in ipairs(playersInvitedRecently) do
if cachedPlayer.name == name_without_realm and cachedPlayer.time + 300 > time() then
self:DebugPrint("Already invited", name, "in the last 5 minutes")
return
end
end
end
-- used to check if we should invite with or without realm name below
-- due to the fact that era has mega servers (multiple realms in one server)
-- where we have to invite with the realm name
-- since those do not exist on anniversary servers and where HasActiveSeason() is true
-- we can validate it like this for now.
local isSeasonal = C_Seasons.HasActiveSeason()
---@diagnostic disable-next-line: undefined-global
if not isHighPriorityRequest and (not self.db.profile.inviteWhisper or not currentLayer or currentLayer <= 0) then
self:DebugPrint(
"Auto-whisper is turned off or we can't provide a helpful whisper, delaying our invite by 500 miliseconds")
C_Timer.After(0.5, function()
if isSeasonal then
InviteUnit(name_without_realm)
else
InviteUnit(name)
end
end)
else
if isSeasonal then
InviteUnit(name_without_realm)
else
InviteUnit(name)
end
end
table.insert(recentLayerRequests, { name = name_without_realm, time = time() })
self:DebugPrint("Added", name_without_realm, "to list of recent layer requests")
local max_group_size = 4
if IsInRaid() then
max_group_size = 39
end
-- check if group is full
if self.db.profile.autokick and GetNumGroupMembers() == max_group_size then
self:DebugPrint("Group is full, kicking")
-- kick last member of raid
local lastMember = GetRaidRosterInfo(GetNumGroupMembers())
table.insert(kicked_player_queue, lastMember)
return
end
end
---@diagnostic disable-next-line: inject-field
function AutoLayer:ProcessSystemMessages(_, SystemMessages)
if not self.db.profile.enabled then
return
end
characterName = SystemMessages:match('^'..ERR_JOINED_GROUP_S:format('(.+)'))
-- X joins the party
if characterName then
local playerNameWithoutRealm = removeRealmName(characterName)
self:DebugPrint("ERR_JOINED_GROUP_S", playerNameWithoutRealm, "found !")
-- Do AutoLayer stuff only if they actually asked for a layer
-- (this may be a normal player we're inviting for different reasons)
for i, entry in ipairs(recentLayerRequests) do
if entry.name == playerNameWithoutRealm then
self.db.profile.layered = self.db.profile.layered + 1
table.insert(playersInvitedRecently, { name = playerNameWithoutRealm, time = time() - 100 })
break -- Found the player, no need to continue checking
end
end
end
characterName = SystemMessages:match('^'..ERR_DECLINE_GROUP_S:format('(.+)'))
-- X declines your invite
if characterName then
local playerNameWithoutRealm = removeRealmName(characterName)
self:DebugPrint("ERR_DECLINE_GROUP_S", playerNameWithoutRealm, "found !")
table.insert(playersInvitedRecently, { name = playerNameWithoutRealm, time = time() }) --Extend this timer, they don't want in right now
self:DebugPrint("Adding ", playerNameWithoutRealm, " to cache, reason: declined invite")
end
characterName = SystemMessages:match('^'..ERR_INVITE_PLAYER_S:format('(.+)'))
if characterName then
local playerNameWithoutRealm = removeRealmName(characterName)
self:DebugPrint("ERR_INVITE_PLAYER_S", playerNameWithoutRealm, "found !")
if self.db.profile.inviteWhisper then
local currentLayer = AutoLayer:getCurrentLayer()
-- I guess don't whisper people if we don't know what layer we're in?
if currentLayer == nil or currentLayer <= 0 then
self:DebugPrint("Not whispering since we don't know what layer we're in! (", currentLayer, ")")
return
end
-- Don't whisper the player unless they specifically asked for a layer
-- (this may be a normal player we're inviting for different reasons)
local isPlayerInvited = false
for i, entry in ipairs(recentLayerRequests) do
if entry.name == playerNameWithoutRealm then
isPlayerInvited = true
break -- Found the player, no need to continue checking
end
end
if not isPlayerInvited then
return
end
-- Continue with the rest of the function if the player is in the list
local finalMessage = "[AutoLayer] " .. string.format(self.db.profile.inviteWhisperTemplate, currentLayer)
CTL:SendChatMessage("NORMAL", characterName, finalMessage, "WHISPER", nil, characterName)
end
if self.db.profile.inviteWhisperReminder then
local finalMessage2 = "[AutoLayer] " .. string.format(self.db.profile.inviteWhisperTemplateReminder)
CTL:SendChatMessage("NORMAL", characterName, finalMessage2, "WHISPER", nil, characterName)
end
end
end
function AutoLayer:HandleAutoKick()
if not self.db.profile.enabled then
return
end
if self.db.profile.autokick and #kicked_player_queue >= 0 then
local name = table.remove(kicked_player_queue, 1)
if name == nil then
return
end
if not UnitInRaid(name) and not UnitInParty(name) then
return
end
self:DebugPrint("Kicking ", name)
UninviteUnit(name)
end
end
function AutoLayer:ProcessRosterUpdate()
self:getCurrentLayer()
end
AutoLayer:RegisterEvent("CHAT_MSG_CHANNEL", "ProcessMessage")
AutoLayer:RegisterEvent("CHAT_MSG_WHISPER", "ProcessMessage")
AutoLayer:RegisterEvent("CHAT_MSG_GUILD", "ProcessMessage")
AutoLayer:RegisterEvent("CHAT_MSG_SYSTEM", "ProcessSystemMessages")
AutoLayer:RegisterEvent("GROUP_ROSTER_UPDATE", "ProcessRosterUpdate")
function JoinLayerChannel()
JoinChannelByName("layer")
local channel_num = GetChannelName("layer")
if channel_num == 0 then
print("Failed to join Layer channel")
else
-- print("Successfully joined Layer channel.")
end
for i = 1, 10 do
if _G['ChatFrame' .. i] then
ChatFrame_RemoveChannel(_G['ChatFrame' .. i], "layer")
end
end
end
function ProccessQueue()
AutoLayer:HandleAutoKick()
if #addonTable.send_queue > 0 then
local payload = table.remove(addonTable.send_queue, 1)
local l_channel_num = GetChannelName("layer")
if l_channel_num == 0 then
JoinLayerChannel()
do return end
end
CTL:SendChatMessage("BULK", "layer", payload, "CHANNEL", nil, l_channel_num)
end
end
C_Timer.After(1, function()
WorldFrame:HookScript("OnMouseDown", function(self, button)
local l_channel_num = GetChannelName("layer")
if l_channel_num == 0 then
JoinLayerChannel()
do return end
end
AutoLayer:HandleAutoKick()
ProccessQueue()
end)
end)
local f = CreateFrame("Frame", "Test", UIParent)
f:SetScript("OnKeyDown", ProccessQueue)
f:SetPropagateKeyboardInput(true)