-
-
Notifications
You must be signed in to change notification settings - Fork 30
/
Copy pathcore.lua
3014 lines (2706 loc) · 113 KB
/
core.lua
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
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--- core.lua Contains core elements of the addon
-- @author Potdisc
--[[AceEvent-3.0 Messages:
core:
RCCouncilChanged - fires when the council changes.
RCConfigTableChanged - fires when the user changes a settings. args: [val]; a few settings supplies their name.
RCUpdateDB - fires when the user receives sync data from another player.
RCLootTableAdditionsReceived - fires when additional lootTable data has been received and processed.
ml_core:
RCMLAddItem - fires when an item is added to the loot table. args: item, loottable entry
RCMLAwardSuccess - fires when an item is successfully awarded. args: session, winner, status, link, responseText.
RCMLAwardFailed - fires when an item is unsuccessfully awarded. args: session, winner, status, link, responseText.
RCMLBuildMLdb - fires just before the MLdb is built. arg: MLdb, the master looter db table.
RCMLLootHistorySend - fires just before loot history is sent out. args: loot_history table (the table sent to users), all arguments from ML:TrackAndLogLoot()
votingFrame:
RCSessionChangedPre - fires when the user changes the session, just before SwitchSession() is executed. args: sesion.
RCSessionChangedPost - fires when the user changes the session, after SwitchSession() is executed. args: session.
lootHistory:
RCHistory_ResponseEdit - fires when the user edits the response of a history entry. args: data (see LootHistory:BuildData())
RCHistory_NameEdit - fires when the user edits the receiver of a history entry. args: data.
ItemStorage:
RCItemStorageInitialized - fires when the item storage is initialized. args: #itemStorage.
]]
--[[ Notable Comm messages: (See Classes/Services/Comms.lua for subscribing to comms)
Comms:
P: Permanent, T: Temporary
MAIN:
StartHandleLoot P - Sent whenever RCLootCouncil starts handling loot.
StopHandleLoot P - Sent whenever RCLootCouncil stops handling loot.
council P - Council received from ML
session_end P - ML has ended the session.
playerInfoRequest P - Request for playerInfo
pI P - Player Info
n_t P - Candidate received "non-tradeable" loot.
r_t P - Candidate "rejected_trade" of loot.
lootTable P - LootTable sent from ML.
lt_add P - Partial lootTable (additions) sent from ML.
mldb P - MLDB sent from ML.
reroll P - (Partial) lootTable with items we should reroll on.
re_roll P - (Partial) lootTable and list of candidates that should reroll.
lootAck P - LootAck received from another player. Used for checking if have received the required data.
Rgear P - Anyone requests our currently equipped gear.
bonus_roll P - Sent whenever we do a bonus roll.
getCov P - Anyone request or covenant ID.
]]
-- GLOBALS: GetLootMethod, C_AddOns.GetAddOnMetadata, UnitClass
local addonname, addontable = ...
--- @class RCLootCouncil : AceAddon, AceConsole-3.0, AceEvent-3.0, AceHook-3.0, AceTimer-3.0, AceBucket-3.0
_G.RCLootCouncil = LibStub("AceAddon-3.0"):NewAddon(addontable, addonname, "AceConsole-3.0", "AceEvent-3.0",
"AceHook-3.0", "AceTimer-3.0", "AceBucket-3.0");
local LibDialog = LibStub("LibDialog-1.1")
--- @type RCLootCouncilLocale
local L = LibStub("AceLocale-3.0"):GetLocale("RCLootCouncil")
local tooltipForParsing = CreateFrame("GameTooltip", "RCLootCouncil_Tooltip_Parse", nil, "GameTooltipTemplate")
tooltipForParsing:UnregisterAllEvents() -- Don't use GameTooltip for parsing, because GameTooltip can be hooked by other addons.
RCLootCouncil:SetDefaultModuleState(false)
local Comms = RCLootCouncil.Require "Services.Comms"
local Council = RCLootCouncil.Require "Data.Council"
local Player = RCLootCouncil.Require "Data.Player"
local MLDB = RCLootCouncil.Require "Data.MLDB"
local TT = RCLootCouncil.Require "Utils.TempTable"
local ItemUtils = RCLootCouncil.Require "Utils.Item"
-- Init shorthands
local db, debugLog; -- = self.db.profile, self.db.global.log
-- init modules
---@enum (key) DefaultModules
local defaultModules = {
masterlooter = "RCLootCouncilML",
lootframe = "RCLootFrame",
history = "RCLootHistory",
version = "VersionCheck",
sessionframe = "RCSessionFrame",
votingframe = "RCVotingFrame",
tradeui = "TradeUI",
sync = "Sync",
}
---@enum (key) UserModules
local userModules = {
masterlooter = nil,
lootframe = nil,
history = nil,
version = nil,
sessionframe = nil,
votingframe = nil,
tradeui = nil,
}
local unregisterGuildEvent = false
local lootTable = {}
-- Lua
local time, tonumber, unpack, select, wipe, pairs, ipairs, format, table, tinsert, tremove, bit, tostring, type = time,
tonumber,
unpack,
select,
wipe,
pairs,
ipairs,
format,
table,
tinsert,
tremove,
bit,
tostring,
type
local playersData = { -- Update on login/encounter starts. it stores the information of the player at that moment.
gears = {}, -- Gears key: slot number(1-19), value: item link
relics = {}, -- Relics key: slot number(1-3), value: item link
} -- player's data that can be changed by the player (spec, equipped ilvl, gaers, relics etc)
function RCLootCouncil:OnInitialize()
self.Log = self.Require "Utils.Log":New()
-- IDEA Consider if we want everything on self, or just whatever modules could need.
self.version = C_AddOns.GetAddOnMetadata("RCLootCouncil", "Version")
self.nnp = false
self.debug = false
self.tVersion = nil -- String or nil. Indicates test version, which alters stuff like version check. Is appended to 'version', i.e. "version-tVersion" (max 10 letters for stupid security)
self.playerClass = select(2, UnitClass("player")) -- TODO: Remove - contained in self.player
self.guildRank = L["Unguilded"]
self.guildName = nil
self.isMasterLooter = false -- Are we the ML?
---@type Player
self.masterLooter = nil -- Masterlooter
self.lootMethod = GetLootMethod() or "personalloot"
self.handleLoot = false -- Does RC handle loot(Start session from loot window)?
self.isCouncil = false -- Are we in the Council?
self.enabled = true -- turn addon on/off
self.inCombat = false -- Are we in combat?
self.recentReconnectRequest = false
self.currentInstanceName = ""
self.bossName = nil -- Updates after each encounter
self.lootOpen = false -- is the ML lootWindow open or closed?
self.lootSlotInfo = {} -- Items' data currently in the loot slot. Need this because inside LOOT_SLOT_CLEARED handler, GetLootSlotLink() returns invalid link.
self.nonTradeables = {} -- List of non tradeable items received since the last ENCOUNTER_END
self.lastEncounterID = nil
self.autoGroupLootWarningShown = false
self.isInGuildGroup = false -- Is the group leader a member of our guild?
---@type table<string,boolean>
self.candidatesInGroup = {}
self.mldb = {} -- db recived from ML
self.chatCmdHelp = {
{cmd = "config", desc = L["chat_commands_config"]},
{cmd = "council", desc = L["chat_commands_council"]},
{cmd = "history", desc = L["chat_commands_history"]},
{cmd = "open", desc = L["chat_commands_open"]},
{cmd = "profile", desc = L.chat_commands_profile, },
{cmd = "reset", desc = L["chat_commands_reset"]},
{cmd = "sync", desc = L["chat_commands_sync"]},
{cmd = "trade", desc = L.chat_commands_trade},
{cmd = "version", desc = L["chat_commands_version"]},
}
self.mlChatCmdHelp = {
{cmd = "add [item]", desc = L["chat_commands_add"]},
{cmd = "add all", desc = L["chat_commands_add_all"]},
{cmd = "award", desc = L["chat_commands_award"]},
{cmd = "clear", desc = L.chat_commands_clear},
{cmd = "export", desc = L.chat_commands_export},
{cmd = "list", desc = L.chat_commands_list},
{cmd = "remove [index]", desc = L.chat_commands_remove},
{cmd = "session", desc = L.chat_commands_session},
{cmd = "start", desc = L.chat_commands_start},
{cmd = "stop", desc = L.chat_commands_stop},
{cmd = "test (#)", desc = L["chat_commands_test"]},
{cmd = "whisper", desc = L["chat_commands_whisper"]},
}
self.lootGUIDToIgnore = { -- List of GUIDs we shouldn't register loot from
["317400"] = true, -- Opulence BoD (trash piles)
}
-- List of item classes all auto looting should ignore
-- see https://wow.gamepedia.com/ItemType
self.blacklistedItemClasses = {
[0] = { -- Consumables
all = true,
},
[5] = { -- Reagents
[0] = true, -- Reagent
[1] = true, -- Keystone
},
[7] = { -- Tradeskills
all = true,
},
[12] = { -- Quest
all = true,
},
[15] = { -- Misc
[1] = true, -- Reagent
[4] = true, -- Other (Anima)
},
}
-- List of itemIds that should not be blacklisted
self.blackListOverride = {
-- [itemId] = true
[206046] = true, -- Void-Touched Curio
[210947] = true, -- Flame-Warped Curio
}
self.testMode = false;
-- create the other buttons/responses
for i = 1, self.defaults.profile.maxButtons do
if i > self.defaults.profile.buttons.default.numButtons then
tinsert(self.defaults.profile.buttons.default, {text = L["Button"] .. " " .. i, whisperKey = "" .. i})
tinsert(self.defaults.profile.responses.default, {color = {0.7, 0.7, 0.7, 1}, sort = i, text = L["Button"] .. i})
end
end
-- create the other AwardReasons
for i = #self.defaults.profile.awardReasons + 1, self.defaults.profile.maxAwardReasons do
tinsert(self.defaults.profile.awardReasons,
{color = {1, 1, 1, 1}, disenchant = false, log = true, sort = 400 + i, text = "Reason " .. i})
end
-- init db
self.db = LibStub("AceDB-3.0"):New("RCLootCouncilDB", self.defaults, true)
self:InitLogging()
self.lootDB = LibStub("AceDB-3.0"):New("RCLootCouncilLootDB")
--[[ Format:
"playerName" = {
[#] = {"lootWon", "date (d/m/y)", "time (h:m:s)", "instance", "boss", "votes", "itemReplaced1", "itemReplaced2", "response", "responseID",
"color", "class", "isAwardReason", "difficultyID", "mapID", "groupSize", "tierToken"}
},
]]
self.db.RegisterCallback(self, "OnProfileChanged", "UpdateDB")
self.db.RegisterCallback(self, "OnProfileCopied", "UpdateDB")
self.db.RegisterCallback(self, "OnProfileReset", "UpdateDB")
self:ClearOldVerTestCandidates()
self:InitClassIDs()
self:InitTrinketData()
-- add shortcuts
db = self.db.profile
debugLog = self.db.global.log
-- Slash setup
if db.useSlashRC then -- Allow presevation of readycheck shortcut.
self:RegisterChatCommand("rc", "ChatCommand")
end
self:RegisterChatCommand("rclc", "ChatCommand")
self.customChatCmd = {} -- Modules that wants their cmds used with "/rc"
-- Register Core Comms
Comms:Register(self.PREFIXES.MAIN)
Comms:Register(self.PREFIXES.VERSION)
self.Send = Comms:GetSender(self.PREFIXES.MAIN)
self:SubscribeToPermanentComms()
-- Add logged in message in the log
self.Log("Logged In")
self:ModulesOnInitialize()
end
function RCLootCouncil:OnEnable()
if not self:IsCorrectVersion() then
self.Log:e("Wrong game version", WOW_PROJECT_ID)
self:Print(format("This version of %s is not intended for this game version!\nPlease install the proper version.",
self.baseName))
return self:Disable()
end
-- Register the player's name
self.realmName = select(2, UnitFullName("player")) -- TODO Remove
if self.realmName == "" then -- Noticed this happening with starter accounts. Not sure if it's a real problem.
self:ScheduleTimer(function() self.realmName = select(2, UnitFullName("player")) end, 2)
end
self.player = Player:Get("player")
self.playerName = self.player:GetName() -- TODO Remove
self.Log(self.playerName, self.version, self.tVersion)
self.EJLatestInstanceID = self:GetEJLatestInstanceID()
self:DoChatHook()
-- register the optionstable
LibStub("AceConfigRegistry-3.0"):RegisterOptionsTable("RCLootCouncil", function() return self:OptionsTable() end)
-- add it to blizz options
self.optionsFrame = LibStub("AceConfigDialog-3.0"):AddToBlizOptions("RCLootCouncil", "RCLootCouncil", nil, "settings")
self.optionsFrame.ml = LibStub("AceConfigDialog-3.0"):AddToBlizOptions("RCLootCouncil", "Master Looter",
"RCLootCouncil", "mlSettings")
self.playersData = playersData -- Make it globally available
self:ScheduleTimer("InitItemStorage", 5, self) -- Delay to have a better change of getting correct item info
-- register events
for event, method in pairs(self.coreEvents) do self:RegisterEvent(event, method) end
self:RegisterBucketEvent("GROUP_ROSTER_UPDATE", 5, "UpdateCandidatesInGroup")
if IsInGuild() then
self.guildName, self.guildRank = GetGuildInfo("player")
self:ScheduleTimer("SendGuildVerTest", 2) -- send out a version check after a delay
end
-- For some reasons all frames are blank until ActivateSkin() is called, even though the values used
-- in the :CreateFrame() all :Prints as expected :o
self:ActivateSkin(db.currentSkin)
if self.db.global.version then -- Intentionally run before updating global.version
self.Compat:Run() -- Do compatibility changes
end
if self:VersionCompare(self.db.global.version, self.version) then self.db.global.oldVersion = self.db.global.version end
self.db.global.version = self.version
if self.db.global.tVersion and self.debug then -- recently ran a test version, so reset debugLog
self.db.global.log = {}
end
self.db.global.locale = GetLocale() -- Store locale in log. Important information for debugging.
self.db.global.regionID = GetCurrentRegion()
self.db.global.tVersion = self.tVersion;
self.Utils:GuildRoster()
local filterFunc = function(_, event, msg, player, ...) return strfind(msg, "[[RCLootCouncil]]:") end
ChatFrame_AddMessageEventFilter("CHAT_MSG_WHISPER_INFORM", filterFunc)
self:CouncilChanged() -- Call to initialize council
end
function RCLootCouncil:OnDisable()
self.Log("OnDisable()")
self:UnregisterChatCommand("rc")
self:UnregisterChatCommand("rclc")
self:UnregisterAllEvents()
Comms:OnDisable()
end
function RCLootCouncil:ConfigTableChanged(val)
--[[ NOTE By default only ml_core needs to know about changes to the config table,
but we'll use AceEvent incase future modules also wants to know ]]
self:SendMessage("RCConfigTableChanged", val)
end
function RCLootCouncil:CouncilChanged()
local council = {}
for _, guid in ipairs(self.db.profile.council) do
local player = Player:Get(guid)
if player and player.guid then council[player.guid] = player end
end
Council:Set(council)
self:SendMessage("RCCouncilChanged")
end
local function validateChatFrame() return type(db.chatFrameName) == "string" and getglobal(db.chatFrameName) end
function RCLootCouncil:DoChatHook()
-- Unhook if already hooked:
if self:IsHooked(self, "Print") then self:Unhook(self, "Print") end
if not validateChatFrame() then
self:Print("Warning: Your chat frame", db.chatFrameName, "doesn't exist. ChatFrame has been reset.")
self.Log:e("ChatFrameName validation failed, resetting...")
db.chatFrameName = self.defaults.profile.chatFrameName
end
-- Pass our channel to the original function and magic appears.
self:RawHook(self, "Print", function(_, ...) self.hooks[self].Print(self, getglobal(db.chatFrameName), ...) end, true)
end
function RCLootCouncil:PrintMLChatHelp()
print ""
local mlCommandsString = self:IsCorrectVersion() and L.chat_commands_groupLeader_only or L.chat_commands_ML_only
print("|cFFFFA500" .. mlCommandsString .. "|r")
for _, y in ipairs(self.mlChatCmdHelp) do
print("|cff20a200", y.cmd, "|r:", y.desc)
end
end
function RCLootCouncil:ChatCommand(msg)
local input = self:GetArgs(msg, 1)
local args = {}
local arg
local startpos = input and #input + 1 or 0
repeat
arg, startpos = self:GetArgs(msg, 1, startpos)
if arg then table.insert(args, arg) end
until arg == nil
input = strlower(input or "")
self.Log:d("/", input, unpack(args))
if not input or input:trim() == "" or input == "help" or input == string.lower(_G.HELP_LABEL) then
if self.tVersion then
print(format(L["chat tVersion string"], self.version, self.tVersion))
else
print(format(L["chat version String"], self.version))
end
local module, shownMLCommands
for _, v in ipairs(self.chatCmdHelp) do
-- Show ML commands beneath regular commands, but above module commands
if v.module and not shownMLCommands then -- this won't trigger if there's no module(s)
self:PrintMLChatHelp()
shownMLCommands = true
end
if v.module ~= module then -- Print module name and version
print "" -- spacer
if v.module.version and v.module.tVersion then
print(v.module.baseName, "|cFFFFA500", v.module.version, v.module.tVersion)
elseif v.module.version then
print(v.module.baseName, "|cFFFFA500", v.module.version)
else
print(v.module.baseName, "|cFFFFA500", C_AddOns.GetAddOnMetadata(v.module.baseName, "Version"))
end
end
if v.cmd then
print("|cff20a200", v.cmd, "|r:", v.desc)
else
print(v.desc) -- For backwards compatibility
end
module = v.module
end
-- If there's no modules, just print the ML commands now
if not shownMLCommands then self:PrintMLChatHelp() end
self.Log:d("- debug or d - Toggle debugging")
self.Log:d("- log - display the debug log")
self.Log:d("- clearLog - clear the debug log")
elseif input == 'config' or input == L["config"] or input == "c" or input == "opt" or input == "options" then
Settings.OpenToCategory(self.optionsFrame.name)
-- LibStub("AceConfigDialog-3.0"):Open("RCLootCouncil")
elseif input == 'debug' or input == 'd' then
self.debug = not self.debug
self:Print("Debug = " .. tostring(self.debug))
elseif input == 'open' or input == L["open"] then
if self.isCouncil or self.mldb.observe or self.nnp then -- only the right people may see the window during a raid since they otherwise could watch the entire voting
self:GetActiveModule("votingframe"):Show()
elseif #lootTable == 0 then
self:Print(L["No session running"])
else
self:Print(L["You are not allowed to see the Voting Frame right now."])
end
elseif input == 'council' or input == L["council"] then
local category = FindValueInTableIf(
SettingsPanel:GetCategory(self.optionsFrame.name):GetSubcategories(),
function(v)
return v and v:GetID() == self.optionsFrame.ml.name
end)
if not category then return self.Log:e("Couldn't find category in '/rc council'", category) end
Settings.OpenToCategory(self.optionsFrame.name)
SettingsPanel:SelectCategory(category)
LibStub("AceConfigDialog-3.0"):SelectGroup("RCLootCouncil", "mlSettings", "councilTab")
elseif input == "profile" or input == "profiles" then
Settings.OpenToCategory(self.optionsFrame.name)
LibStub("AceConfigDialog-3.0"):SelectGroup("RCLootCouncil", "settings", "profiles")
elseif input == 'test' or input == L["test"] then
self:Test(tonumber(args[1]) or 1)
elseif input == 'fulltest' or input == 'ftest' then
self:Test(tonumber(args[1]) or 1, true)
elseif input == 'version' or input == L["version"] or input == "v" or input == "ver" then
if args[1] then -- Print outdated versions
self:GetActiveModule("version"):PrintOutDatedClients()
else -- Otherwise open it
self:CallModule("version")
end
elseif input == "history" or input == string.lower(_G.HISTORY) or input == "h" or input == "his" or input == "hist" then
self:CallModule("history")
elseif input == "whisper" or input == string.lower(_G.WHISPER) then
self:Print(L["whisper_help"])
elseif input == "add" or input == string.lower(_G.ADD) then
if not args[1] or args[1] == "" then return self:ChatCommand("help") end
if self.isMasterLooter then
self:ChatCmdAdd(args)
else
self:Print(L["You cannot use this command without being the Master Looter"])
end
elseif input == "award" or input == L["award"] then
if self.isMasterLooter then
self:GetActiveModule("masterlooter"):SessionFromBags()
else
self:Print(L["You cannot use this command without being the Master Looter"])
end
elseif input == "list" then -- Print db.baggedItems
if self.isMasterLooter then
self:GetActiveModule("masterlooter"):PrintItemsInBags()
else
self:Print(L["You cannot use this command without being the Master Looter"])
end
elseif input == "remove" then -- Remove one or more entries from db.baggedItems
if self.isMasterLooter then
self:GetActiveModule("masterlooter"):RemoveItemsInBags(unpack(args))
else
self:Print(L["You cannot use this command without being the Master Looter"])
end
elseif input == "clear" then -- Clear db.baggedItems
if self.isMasterLooter then
self:GetActiveModule("masterlooter"):ClearAllItemsInBags()
else
self:Print(L["You cannot use this command without being the Master Looter"])
end
elseif input == "hidelootframe" or input == "hidelootframes" then
self.Require "Utils.GroupLoot":HideGroupLootFrames()
elseif input == "reset" or input == string.lower(_G.RESET) then
self:ResetUI()
self:Print(L["Windows reset"])
elseif input == "start" or input == string.lower(_G.START) then
if self.Utils.IsPartyLFG() then
return self:Print(L.chat_command_start_error_start_PartyIsLFG)
elseif db.usage.never then
return self:Print(L.chat_command_start_error_usageNever)
elseif not IsInRaid() and db.onlyUseInRaids then
return self:Print(L.chat_command_start_error_onlyUseInRaids)
elseif not self.isMasterLooter then
return self:Print(L["You cannot use this command without being the Master Looter"])
end
-- Simply emulate player entering raid.
self:OnRaidEnter()
elseif input == "stop" or input == string.lower(L.Stop) then
if self.isMasterLooter then
if self.handleLoot then
self:StopHandleLoot()
else
self:Print(L.chatCommand_stop_error_notHandlingLoot)
end
else
self:Print(L["You cannot use this command without being the Master Looter"])
end
elseif input == "debuglog" or input == "log" then
for k, v in ipairs(debugLog) do print(k, v); end
elseif input == "clearlog" then
wipe(debugLog)
self:Print("Debug Log cleared.")
elseif input == "clearcache" then
self.db.global.cache = {}
self:Print("Cache cleared")
elseif input == "sync" then
self.Sync:Enable()
elseif input == "session" or input == "ses" or input == "s" then
if self.isMasterLooter then
self:GetActiveModule("masterlooter"):ShowSessionFrame()
else
self:Print(L["You cannot use this command without being the Master Looter"])
end
elseif input == "trade" then
self.TradeUI:Show(true)
elseif input == "safemode" then
db.safemode = not db.safemode
self:Print("SafeMode " .. (db.safemode and "On" or "Off"))
elseif input == "export" then
self:ExportCurrentSession()
elseif input == "unlock" then
if not args[1]then
return self:Print("Must have 'item' as second argument.")
end
self:UnlockItem(args[1])
--@debug@
elseif input == "nnp" then
self.nnp = not self.nnp
self:Print("nnp = " .. tostring(self.nnp))
elseif input == "exporttrinketdata" then
self:ExportTrinketData(tonumber(args[1]), 0, tonumber(args[2]), 1)
elseif input == "trinkettest" or input == "ttest" then
self.playerClass = string.upper(args[1])
self:Test(1, false, true)
elseif input == "exporttokendata" then
self:ExportTokenData(tonumber(args[1]))
elseif input == 't' then -- Tester cmd
-- Test items with several modifiers. Should probably be added to the regular test func
for i = 1, GetNumGuildMembers() do
local info = {GetGuildRosterInfo(i)}
if (info[9]) then
local guid = info[17]
local inGuild = IsInGuild(guid)
print(Player:Get(guid), "in guild = ", inGuild)
end
end
--@end-debug@
else
-- Check if the input matches anything
for k, v in pairs(self.customChatCmd) do if k == input then return v.module[v.func](v.module, unpack(args)) end end
self:ChatCommand("help")
end
end
-- Items in here should not be handled by `UpdateAndSendRecentTradableItem`.
local itemsBeingGroupLooted = {}
RCLootCouncil.Require ("Utils.GroupLoot").OnLootRoll:subscribe(function (link)
tinsert(itemsBeingGroupLooted, link)
end)
-- Update the recentTradableItem by link, if it is in bag and tradable.
-- Except when the item has been auto group looted.
function RCLootCouncil:UpdateAndSendRecentTradableItem(info, count)
local index = tIndexOf(itemsBeingGroupLooted, info.link)
if index then
-- Remove from list in case we get future similar items.
tremove(itemsBeingGroupLooted, index)
return
end
local Item = self.ItemStorage:New(info.link, "temp")
self.ItemStorage:WatchForItemInBags(Item, function() -- onFound
self:LogItemGUID(Item)
if Item.time_remaining > 0 then
Item:Store()
if self.mldb.rejectTrade and IsInRaid() then
LibDialog:Spawn("RCLOOTCOUNCIL_KEEP_ITEM", info.link)
return
end
self:Send("group", "tradable", info.link, info.guid)
return
end
-- We've searched every single bag space, and found at least 1 item that wasn't tradeable,
-- and none that was. We can now safely assume the item can't be traded.
self:Send("group", "n_t", info.link, info.guid)
end, function() -- onFail
-- We haven't found it, maybe we just haven't received it yet, so try again in one second
Item:Unstore()
self.Log:e(format("UpdateAndSendRecentTradableItem: %s not found in bags", Item.link))
end)
end
-- Send the msg to the channel if it is valid. Otherwise just print the messsage.
function RCLootCouncil:SendAnnouncement(msg, channel)
if channel == "NONE" then return end
if self.testMode then msg = "(" .. L["Test"] .. ") " .. msg end
if (not IsInGroup()
and (channel == "group" or channel == "RAID" or channel == "RAID_WARNING" or channel == "PARTY" or channel
== "INSTANCE_CHAT")) or channel == "chat" or (not IsInGuild() and (channel == "GUILD" or channel == "OFFICER")) then
self:Print(msg)
elseif (not IsInRaid() and (channel == "RAID" or channel == "RAID_WARNING")) then
SendChatMessage(msg, "PARTY")
else
SendChatMessage(msg, self.Utils:GetAnnounceChannel(channel))
end
end
function RCLootCouncil:ResetReconnectRequest()
self.recentReconnectRequest = false
self.Log:d("ResetReconnectRequest")
end
function RCLootCouncil:ChatCmdAdd(args)
if not args[1] or args[1] == "" then return end -- We need at least 1 arg
-- Add all items in bags with trade timers
if args[1] == "bags" or args[1] == "all" then
local items = self:GetAllItemsInBagsWithTradeTimer()
self:Print(format(L["chat_cmd_add_found_items"], #items))
local player = self.player:GetName() -- Save the extra calls
for _, v in ipairs(items) do self:GetActiveModule("masterlooter"):AddUserItem(v, player) end
return
end
-- Add linked items
local owner
-- See if one of the args is a owner
if not args[1]:find("|") and type(tonumber(args[1])) ~= "number" then
-- First arg is neither an item or a item id, see if it's someone in our group
owner = self:UnitName(args[1])
if not (owner and owner ~= "" and self.candidatesInGroup[owner]) then
self:Print(format(L["chat_cmd_add_invalid_owner"], owner))
return
end
tremove(args, 1)
end
-- Now handle the links
local links = args
if args[1]:find("|h") then -- Only split links if we have at least one
links = self:SplitItemLinks(args) -- Split item links to allow user to enter links without space
end
for _, v in ipairs(links) do self:GetActiveModule("masterlooter"):AddUserItem(v, owner or self.player:GetName()) end
end
-- if fullTest, add items in the encounterJournal to the test items.
function RCLootCouncil:Test(num, fullTest, trinketTest)
self.Log:d("Test", num)
local testItems = {
-- Tier21 Tokens (Head, Shoulder, Cloak, Chest, Hands, Legs)
152524,
152530,
152517,
152518,
152521,
152527, -- Vanquisher: DK, Druid, Mage, Rogue
152525,
152531,
152516,
152519,
152522,
152528, -- Conqueror : DH, Paladin, Priest, Warlock
152526,
152532,
152515,
152520,
152523,
152529, -- Protector : Hunder, Monk, Shaman, Warrior
-- Tier21 Armors (Head, Shoulder, Chest, Wrist, Hands, Waist, Legs, Feet)
152014,
152019,
152017,
152023,
152686,
152020,
152016,
152009, -- Plate
152423,
152005,
151994,
152008,
151998,
152006,
152002,
151996, -- Mail
151985,
151988,
151982,
151992,
151984,
151986,
151987,
151981, -- Leather
151943,
151949,
152679,
151953,
152680,
151942,
151946,
151939, -- Cloth
-- Tier21 Miscellaneous
152283,
151965,
151973, -- Neck
151937,
151938,
152062, -- Cloak
151972,
152063,
152284, -- Rings
-- Tier21 Relics
152024,
152025, -- Arcane
152028,
152029, -- Blood
152031,
152032, -- Fel
152035,
152036, -- Fire
152039,
152040, -- Frost
152043,
152044, -- Holy
152047,
152048, -- Iron
152050,
152051, -- Life
152054,
152055, -- Shadow
152058,
152059, -- Storm
}
local trinkets = {
-- Tier21 Trinkets
154172, -- All classes
151975,
151976,
151977,
151978,
152645,
153544,
154173, -- Tank
151974, -- Eye of Shatug. EJ item id is different with the item id actually drops
151956,
151957,
151958,
151960,
152289,
154175, -- Healer
151964,
152093, -- Melee DPS
154176, -- Strength DPS
154174, -- Agility DPS
151970, -- Intellect DPS/Healer
151955,
151971,
154177, -- Intellect DPS
151963,
151968, -- Melee and ranged attack DPS
151962,
151969, -- Ranged attack and spell DPS
}
if not trinketTest then for _, t in ipairs(trinkets) do tinsert(testItems, t) end end
if fullTest then -- Add items from encounter journal which includes items from different difficulties.
testItems = {}
C_AddOns.LoadAddOn("Blizzard_EncounterJournal")
local cached = true
local difficulties = {14, 15, 16} -- Normal, Heroic, Mythic
EJ_SelectInstance(self.EJLatestInstanceID)
EJ_ResetLootFilter()
for _, difficulty in pairs(difficulties) do
EJ_SetDifficulty(difficulty)
self.Log:d("EJ_SetDifficulty()", difficulty)
local n = EJ_GetNumLoot()
self.Log:d("EJ_GetNumLoot()", n)
if not n then cached = false end
for i = 1, n or 0 do
local link = C_EncounterJournal.GetLootInfoByIndex(i).link
if link then
tinsert(testItems, link)
else
cached = false
end
end
if not cached then
self.Log:d("Retrieving item info from Encounter Journal. Retry after 1s.")
return self:ScheduleTimer("Test", 1, num, fullTest)
end
end
end
local items = {};
-- pick "num" random items
for i = 1, num do -- luacheck: ignore
local j = math.random(1, #testItems)
tinsert(items, testItems[j])
end
if trinketTest then -- Always test all trinkets.
items = trinkets
end
self.testMode = true;
self.isMasterLooter, self.masterLooter = self:GetML()
-- We must be in a group and not the ML
if not self.isMasterLooter then
self:Print(L.error_test_as_non_leader)
self.testMode = false
return
end
-- Call ML module and let it handle the rest
self:CallModule("masterlooter")
self:GetActiveModule("masterlooter"):NewML(self.masterLooter)
self:GetActiveModule("masterlooter"):Test(items)
end
function RCLootCouncil:EnterCombat()
self.inCombat = true
self.UI:MinimizeFrames()
end
function RCLootCouncil:LeaveCombat()
self.inCombat = false
self.UI:MaximizeFrames()
end
function RCLootCouncil:UpdatePlayersGears(startSlot, endSlot)
startSlot = startSlot or _G.INVSLOT_FIRST_EQUIPPED
endSlot = endSlot or INVSLOT_LAST_EQUIPPED
for i = startSlot, endSlot do
local iLink = GetInventoryItemLink("player", i)
if iLink then
local iName = C_Item.GetItemInfo(iLink)
if iName then
playersData.gears[i] = iLink
else -- Blizzard bug that GetInventoryItemLink returns incomplete link. Retry
self:ScheduleTimer("UpdatePlayersGears", 1, i, i)
end
else
playersData.gears[i] = nil
end
end
end
-- Update player's data which is changable by the player. (specid, equipped ilvl, specs, gears, etc)
function RCLootCouncil:UpdatePlayersData()
self.Log("UpdatePlayersData()")
playersData.specID = GetSpecialization() and GetSpecializationInfo(GetSpecialization())
playersData.ilvl = select(2, GetAverageItemLevel())
self:UpdatePlayersGears()
end
-- @param link A gear that we want to compare against the equipped gears
-- @param gearsTable if specified, compare against gears stored in the table instead of the current equipped gears, whose key is slot number and value is the item link of the gear.
-- @return the gear(s) that with the same slot of the input link.
function RCLootCouncil:GetPlayersGear(link, equipLoc, gearsTable)
self.Log("GetPlayersGear", link, equipLoc)
local GetInventoryItemLink = GetInventoryItemLink
if gearsTable and #gearsTable > 0 then -- lazy code
GetInventoryItemLink = function(_, slotNum) return gearsTable[slotNum] end
end
local itemID = ItemUtils:GetItemIDFromLink(link) -- Convert to itemID
if not itemID then return nil, nil; end
local item1, item2;
-- check if the item is a token, and if it is, return the matching current gear
if RCTokenTable[itemID] then
if RCTokenTable[itemID] == "MultiSlots" then -- Armor tokens for multiple slots, just return nil
return
elseif RCTokenTable[itemID] == "Trinket" then -- We need to return both trinkets
item1 = GetInventoryItemLink("player", GetInventorySlotInfo("TRINKET0SLOT"))
item2 = GetInventoryItemLink("player", GetInventorySlotInfo("TRINKET1SLOT"))
else -- Just return the slot from the tokentable
item1 = GetInventoryItemLink("player", GetInventorySlotInfo(RCTokenTable[itemID]))
end
return item1, item2
end
local slot = self.INVTYPE_Slots[equipLoc]
if not slot then
-- Check if we have a typecode for it
slot = self.INVTYPE_Slots[self:GetTypeCodeForItem(link)]
-- TODO Dirty hack for context tokens. Could do with a better system for both determining typecode and equiploc overrides
local _, _, _, _, _, itemClassID, itemSubClassID = C_Item.GetItemInfoInstant(link)
if itemClassID == 5 and itemSubClassID == 2 then slot = self.INVTYPE_Slots.CONTEXT_TOKEN end
end
if not slot then return nil, nil end
item1 = GetInventoryItemLink("player", GetInventorySlotInfo(slot[1] or slot))
if not item1 and slot['or'] then item1 = GetInventoryItemLink("player", GetInventorySlotInfo(slot['or'])) end
if slot[2] then item2 = GetInventoryItemLink("player", GetInventorySlotInfo(slot[2])) end
return item1, item2;
end
--- Generates a "type code" used to determine which set of buttons to use for the item.
--- The returned code can be used directly in `mldb.responses[code]` and `mldb.buttons[code]`.
--- <br>See [Constants.lua](lua://RCLootCouncil.RESPONSE_CODE_GENERATORS)
--- @param item string|integer Any valid input for [`C_Item.GetItemInfoInstant`](lua://C_Item.GetItemInfoInstant).
--- @return string #The typecode for the item.
function RCLootCouncil:GetTypeCodeForItem(item)
local itemID, _, _, itemEquipLoc, _, itemClassID, itemSubClassID = C_Item.GetItemInfoInstant(item)
if not itemID then return "default" end -- We can't handle uncached items!
for _, func in ipairs(self.RESPONSE_CODE_GENERATORS) do
local val = func(item, db, itemID, itemEquipLoc, itemClassID, itemSubClassID)
if val then return val end
end
-- Remaining is simply their equipLoc, if set
return db.enabledButtons[itemEquipLoc] and itemEquipLoc or "default"
end
-- Sends a response. Uses the gear equipped at the start of most recent encounter or login.
-- @paramsig session [, ...]
-- link, ilvl, equipLoc and subType must be provided to send out gear information.
-- @param target The target of response
-- @param session The session to respond to.
-- @param response The selected response, must be index of db.responses.
-- @param isTier Indicates if the response is a tier response. (v2.4.0) - DEPRECATED
-- @param isRelic Indicates if the response is a relic response. (v2.5.0) - DEPRECATED
-- @param note The player's note.
-- @param roll The player's roll.
-- @param link The itemLink of the item in the session.
-- @param ilvl The ilvl of the item in the session.
-- @param equipLoc The item in the session's equipLoc.
-- @param relicType The type of relic
-- @param sendAvgIlvl Indicates whether we send average ilvl.
-- @param sendSpecID Indicates whether we send spec id.
function RCLootCouncil:SendResponse(target, session, response, isTier, isRelic, note, roll, link, ilvl, equipLoc,
relicType, sendAvgIlvl, sendSpecID)
self.Log:d("SendResponse", target, session, response, isTier, isRelic, note, roll, link, ilvl, equipLoc, relicType,
sendAvgIlvl, sendSpecID)
local g1, g2, diff
if link and ilvl then
g1, g2 = self:GetGear(link, equipLoc, relicType)
diff = self:GetIlvlDifference(link, g1, g2)
end
self:Send(target, "response", session, {
gear1 = g1 and ItemUtils:GetItemStringFromLink(g1) or nil,